随着微服务和 CI 的流行, 在目前的软件工程领域中单元测试可以说是必不可少的一个环节, 在 TDD 中, 单元测试更是被提高到了一个新的高度. 但是很多公司由于很多不同的原因, 没有能持续维护, 或者干脆就从来没有写过单元测试, 确实, 单元测试在初期和代码维护期会需要花一些投入, 但是, 如果一个项目是需要长期维护和更新的, 那么单元测试的作用, 相对于投入来说就根本不算什么. 见过很多人写的单元测试, 虽然也可以运行, 也有覆盖率, 但是稍微分析一下就会看出来, 那根本就不是单元测试, 而已经是集成测试, 比如有人竟然要在单元测试中访问网络, 写文件, 甚至读写数据库..
那么什么样的数据库是好的单元测试呢, 根据笔者的经验, 以下几点可能是必须的:
1. 运行速度快, 对于一个有几百个单元测试用例的测试来说, 我期待 1-2 分钟内可以运行完成, 应为如果我在重构代码, 这可以让我在很快的时间内得到反馈.
2. 不要依赖外部因素, 单元测试只针对单一函数功能测试
3. 一个用例只测试一个函数
对于其中的第二点, 可能是比较麻烦的, 因为, 如果一个函数是类型的成员函数, 那么很可能会依赖很多内部的成员变量, 这种情况就是 mock 出场的时候了, 因为使用 mock 才能让我们专注于自己函数一业务逻辑的测试, 而将依赖隔离开. 笔者使用过很多种语言的 mock 库, 用的最顺手的还是 Java 的 mokito, 当然 c++ 语言也有很多类似的产品, 比如 gmock, fake it, 但是其局限性确实比较多, 如果不在代码开始阶段了解, 并且做好计划, 后期想加入单元测试, 并且使用 gmock 的时候可能就会追悔莫及, 大动干戈, 下面我们来分场景分析一下这些局限性.
场景 1:
- class TurtleReal {
- public:
- void PenUp()
- {}
- void PenDown()
- {
- }
- };
- class MockTurtleReal : public TurtleReal {
- public:
- MOCK_METHOD0(PenUp, void());
- MOCK_METHOD0(PenDown, void());
- };
- class PainterdReal
- {
- TurtleReal* turtle;
- public:
- PainterdReal(TurtleReal* turtle)
- : turtle(turtle) {}
- bool DrawCircle(int, int, int) {
- turtle->PenDown();
- return true;
- }
- };
- TEST(PainterTest, ChildRealCanDrawSomething) {
- MockTurtleReal turtle;
- EXPECT_CALL(turtle, PenDown())
- .Times(AtLeast(1));
- PainterdReal painter(&turtle);
- EXPECT_TRUE(painter.DrawCircle(0, 0, 10));
- }
结果 1:
结论一:
为什么用例会失败呢, gmock 依赖 C++ 多态机制进行工作, 只有虚函数才能被 mock, 非虚函数不能被 mock, 这一点告诉我们, 如果想要在代码中使用 gmock 类的设计中, 最好采用接口隔离, 对于 c++ 来说也就是采用纯虚类型, 因为 c++ 本身没有接口类型.
场景 2:
- class Turtle {
- public:
- virtual ~Turtle() {}
- virtual void PenUp() = 0;
- virtual void PenDown() = 0;
- };
- class MockTurtle : public Turtle {
- public:
- MOCK_METHOD0(PenUp, void());
- MOCK_METHOD0(PenDown, void());
- };
- class Painter
- {
- Turtle* turtle;
- public:
- Painter(Turtle* turtle)
- : turtle(turtle) {}
- bool DrawCircle(int, int, int) {
- turtle->PenDown();
- return true;
- }
- };
- TEST(PainterTest, CanDrawSomething) {
- MockTurtle turtle;
- EXPECT_CALL(turtle, PenDown())
- .Times(AtLeast(1));
- Painter painter(&turtle);
- EXPECT_TRUE(painter.DrawCircle(0, 0, 10));
- }
结果 2:
结论二:
将函数改为虚函数, 测试用例通过
场景 3:
- class TurtleChild: Turtle {
- public:
- void PenUp()
- {
- int a = 0;
- };
- void PenDown()
- {
- int b = 0;
- };
- };
- class MockTurtleChild : public TurtleChild {
- public:
- MOCK_METHOD0(PenUp, void());
- MOCK_METHOD0(PenDown, void());
- };
- class PainterChildRef
- {
- TurtleChild turtle;
- public:
- PainterChildRef(TurtleChild& turtle)
- : turtle(turtle) {}
- bool DrawCircle(int, int, int) {
- turtle.PenDown();
- return true;
- }
- };
- TEST(PainterTest, ChildCanDrawSomething) {
- MockTurtleChild turtle;
- EXPECT_CALL(turtle, PenDown())
- .Times(AtLeast(1));
- PainterChild painter(&turtle);
- EXPECT_TRUE(painter.DrawCircle(0, 0, 10));
- }
结果 3:
结论三:
测试用例通过, 派生类中的同名函数仍然是虚函数, 同样支持多态, 支持 gomck
场景 4:
- class Turtle {
- public:
- virtual ~Turtle() {}
- virtual void PenUp() = 0;
- virtual void PenDown() = 0;
- };
- class TurtleChild: Turtle {
- public:
- void PenUp()
- {
- int a = 0;
- };
- void PenDown()
- {
- int b = 0;
- };
- };
- class MockTurtleChild : public TurtleChild {
- public:
- MOCK_METHOD0(PenUp, void());
- MOCK_METHOD0(PenDown, void());
- };
- class PainterChildRef
- {
- TurtleChild turtle;
- public:
- PainterChildRef(TurtleChild& turtle)
- : turtle(turtle) {}
- bool DrawCircle(int, int, int) {
- turtle.PenDown();
- return true;
- }
- };
- TEST(PainterTest, ChildRefCanDrawSomething) {
- MockTurtleChild turtle;
- EXPECT_CALL(turtle, PenDown())
- .Times(AtLeast(1));
- PainterChildRef painter(turtle);
- EXPECT_TRUE(painter.DrawCircle(0, 0, 10));
- }
结果 4:
结论四:
测试用例失败, 以引用类型传入的成员变量本身不具备多态特性, 因此 gmock 不支持
结论
本文通过四个场景, 层层递进, 深入的剖析了 gmock 的使用, 希望大家在写代码之前早做打算, 避免大动干戈, 返工重来. 但是从另一个方面来说, 接口隔离, p-impl 惯用法等技术, 应该是一个 c++ 老鸟的必备法宝, 可见好多东西都是有其道理的, 前期不了解, 后期只能花更多的精力取弥补, 要么推翻重构, 要么直接放弃, 无知者无畏, no zuo, no die..
来源: https://www.cnblogs.com/pugang/p/9500352.html