单元测试不但可以增加开发者对于所完成代码的自信,同时,好的单元测试用例往往可以在回归测试的过程中,很好地保证之前所发生的修改没有破坏已有的程序逻辑。因此,单元测试不但不会成为开发者的负担,反而可以在保证开发质量的情况下,加速迭代开发的过程。
C++的单元测试框架,我见过最常多的就是gtest了。除此之外,boost也提供了一个用于单元测试的框架,boost仅在学校时使用过,印象中与gtest使用方式大同小异。
单元测试框架通常会对一些变量或函数设置期望,若变量值或返回值符合预期,就认为单元测试用例通过。gtest也提供了下面一些断言:
ASSERT_*系列的断言,当检查点失败时,立即退出单元测试;EXPECT_*系列的断言,当检查点失败时,单元测试还是会继续执行,但结束后会标记所有ECPECT_*失败的用例;EXPECT_CALL设置函数调用之后期望的实现,比如直接返回某一个值。该断言后面没有.Times()时,无论函数有没有调用都不会导致失败,如果有.Times()时,不满足.Times()设置的次数时就会导致期望失败;
有时候对于一些接口,比如向服务器发送请求。但单元测试中有没有可用于测试的服务器,这个时候就需要mock这个请求接口。mock工具的作用是指定函数的行为(模拟函数的行为)。可以对入参进行校验,对出参进行设定,还可以指定函数的返回值。
Mock的基本使用方法是:
因为Mock是基于多态实现的,gmock是不支持Mock全局函数或者静态成员函数的。对于这些全局函数,比较传统的做法是创建一个Wrapper,用虚方法对这些静态函数进行包裹.在测试的时候对Wrapper进行Mock便可控制被包裹的静态函数的行为。
单元测试的case不应该有直接的依赖关系,每一个case在SetUp之后应该达到可以直接测试的条件,在TearDown之后不应该残留任何状态。这里所说的[测试具有依赖关系的case]指的是:一个case测试的条件是另一个case执行正常路径之后的状态。说的有点绕,举个例子:
对于这三个case,测试的时候代码该怎么写呢?难道测case2的时候要把case1的正常路径写到case2的开头?测case3的时候要把case1和case2的正常路径写到case3的开头?这代码得有多臃肿?如果还有case4、case5呢?
对于这种情况,应该充分利用SetUp和TearDown。我会这样写:
TEST_F(AuthenticateTestauthen_success){authenticate_success();}TEST_F(AuthenticateTest,authen_failed){//错误case。这个就没必要封装函数了,因为其他地方也不会用到这个case}添加商品到购物车测试头文件:AddTest.h
classAddTest:publicAuthenticateTest//注意:这里要继承AuthenticateTest,而不是Test,因为AuthenticateTest类中有我们测试添加商品的条件:成功登陆{public:virtualvoidSetUp(){AuthenticateTest::SetUp();authenticate_success();//成功登陆//其他前提条件}virtualvoidTearDown(){//清理环境AuthenticateTest::TearDown();//父类的清理函数}protected:voidadd_success(){//正常case,可以使用EXPECT_*等条件}//如果接下来的case需要一些公共变量};添加商品到购物车测试源文件:AddTest.cpp
TEST_F(AddTestadd_success){add_success();}TEST_F(AddTest,add_failed){//错误case。这个就没必要封装函数了,因为其他地方也不会用到这个case}结账测试头文件:PayTest.h
classPayTest:publicAddTest//注意:这里要继承AddTest,而不是Test,因为AddTest类中有我们测试添加商品的条件:成功添加商品到购物车{public:virtualvoidSetUp(){AddTest::SetUp();add_success();//成功添加商品//其他前提条件}virtualvoidTearDown(){//清理环境AddTest::TearDown();//父类的清理函数}};结账测试源文件:PayTest.cpp
TEST_F(PayTestpay_success){}TEST_F(PayTest,pay_failed){}(3)依赖注入假如Mock类已经写好,那如何把实例化出来的Mock对象传入被测方法呢?(有时候被Mock的类或对象可能在被测接口内部使用)举个例子:需要Mock的类:
classMT{public:virtualvoidconnectDB(){//一些还未实现的,或不好用于测试的操作。比如访问数据库}};被Mock的对象处于被测类内部
classWrapper{public:virtualvoidinit()//如果init方法不是virtual,为了单元测试也要加上virtual。为什么要virtual,下文说{_mt=newMT;//难测点:待Mock类处于函数内,无法通过参数传入//dosomething}virtualvoidtoBeTestFunc()//被测对象{_mt->func();//otheroperator}private:MT*_mt;};Mock:
classMockMT:publicMT{public:MOCK_METHOD0(connectDB,void());}很关键的依赖注入部分,对待测类也进行Mock:
classMockWrapper:publicWrapper//依然需要Mock{public:MOCK_METHOD0(init,void());//这里就是为什么Wrapper::init是virtual的原因,需要MockvoidrealToBeTestFunc()//定义一个函数,但是调用父类的接口,原因在单元测试case中说{Wrapper::toBeTestFunc();}};