【转】各位都是怎么进行单元测试的?

原文:https://www.zhihu.com/question/27313846/answer/130954707

工作中写C++,不敢自称大神,也来斗胆分享(安利)一下经常使用的单元测试框架。

大家都对Google的C++ Style很熟悉了,但除了Coding Style之外,Google还有自己的单元测试框架:gtest (Google Test)和gmock (Google Mock)。

简介gtest的英文Unit Testing C++ with Google Test - ReSharper C++ Blog,英文好的骚年可以直接食用,如果大家确实很需要,我也可以抽空翻译一个。

相关的GitHub链接:google/googletest 可以阅读源码,check out下来使用。

正片

在Google工作,尤其是写C++的程序员,常常离不开写单元测试。所幸的是,Google提供了很成熟,也很实用的单元测试框架gtest和gmock。
gmock是gtest的一部分,也可以说是gtest中比较advanced的topic。至于使用场景(避免在单元测试代码中向production服务发送rpc啦,读取production的资源啦,绕过一些production环境中必须的权限来测试代码的逻辑啦)会在后面举例说明。

正式开始写长一点的C++程序之后,就会逐渐萌发出写unit test来保证代码正确性的需求。例如,一开始写的单元测试可能是这样的。有个函数实现加法:

int Add(int x, int y);

我们最开始会使用assert来做一些基本的测试:

assert(2 == Add(1, 1)); // 正常的用例
assert(1 != Add(1, 1)); // 异常的用例
assert(sum > Add(num1, num2)); // 溢出异常?如果num1和num2都是正整数的前提下,sum应该blablabla

其实在我们一开始学习写单元测试的时候,老师或者有人生经验的资深码农就已经告诉了我们写单元测试的一些原则:

  1. 测试用例能验证函数的正确性(这条都通不过就……);
  2. 测试用例尽可能涵盖边界条件(例如遍历一个链表,头指针是空,只有一个节点,链表有N个节点,N是问题描述下允许的最大节点数等等);
  3. 一些异常和错误处理(例如往一个函数里传入空指针,传入空串,这个函数能否打印一些log,返回错误码,实现加法的Add函数如何检测和处理溢出等等)
    最理想的情况下(时间很多!),应该尽量多写测试用例,以保证代码功能的正确性符合预期,具有良好的容错性。如果代码较复杂,条件分支较多,测试用例最好能覆盖所有的分支路径。
    上述原则一般解决了很多“应该测试什么”的问题。

工作了一些年头,我觉得单元测试起到的最重要作用其实是:让人在修改代码之后能感到安心,踏实(单元测试跑过之后能比用飘柔更自信)。只要跑一把单元测试,就能自动化验证程序逻辑的正确性,而无需在提交代码之前提心吊胆、担心会漏掉什么情况没有处理或者自己新加入的逻辑制造了bug。

其实,写单元测试是很繁琐的。因为要考虑的琐碎的东西其实很多,有时候为了方便,可能还要修改原来已经写好的接口(没错,我菜,我没有先写测试再写实现,我不fashion,我没贯彻test driven development原则,我活该_(:з」∠)_)。

所以,为了能在写单测的时候可以偷点懒,也为了代码读起来舒服一点,gtest框架提供了很多宏

bool IsAI(const string& name);
assert (false == IsAI("vczh")); // 不Fashion!
EXPECT_TRUE(IsAI("Chen meng meng")); // Fashion! 而且有比较直白的英语描述,可读性up

诸如此类的还有:

EXPECT_NE(男朋友爱你, 随时时刻都知道你在想什么); // 真理,男朋友爱你真的不等于他就是你肚子里的蛔虫,想要他买Chanel包包给你做礼物而不是劝你多喝热水的话还是得开口直说!
EXPECT_GT(x, y); // x > y
EXPECT_EQ("光头能对空", GanSi("黄旭东"));
EXPECT_THAT(actual_proto_message, EqualsTo(expected_proto_message)); // EXPECT_THAT(value, matcher). 可以比较一些复杂一点的数据结构例如proto buf,vector里的内容:
vector<int> numlist1;
vector<int> numlist2;
....
// In test
EXPECT_THAT(numlist1, ::testing::ContainerEq(numlist2));

相反,如果写成这样就显示不出我们fashion的一面了:

for (size_t i = 0; i < numlist1.size(); ++i) {
  assert(numlist1[i] == numlist[2]);
}

如果numlist1numlist2里的元素的顺序是乱的,上面那种写法就不可行了。

有人问,你这样写也只不过是看起来好看罢了,还要多打几个字符,没什么必要吧?这正是为什么我们要使用gtest这个框架。因为框架给我们做了额外的事。试想下,如果测试挂了:

ai.cc:233 assert("钱赞企永不为奴", GanSi("黄旭东"));

core dump里也就只告诉你测试挂在了ai.cc这个文件的第233行罢了。但如何你使用的是:EXPECT_EQ("钱赞企永不为奴", GanSi(“黄旭东”));,gtest就会告诉你:

[RUN] XieXingTest.GanSi
SC2_test.cc:233 Failure
Value Of: GanSi(“黄旭东”)
Actual: "光头能对空"
Expected: “ 钱赞企永不为奴”
[Failed] ....

又例如:

高清无码,4K全彩错误提示,你不仅知道哪个test case挂了,你还能知道当前输出是啥,就不用再慢慢打log看了。

框架帮我们做了一些琐碎的事情,我们就能提高工作效率,实现work life balance

而且在使用上,使用gtest也很方便快捷,只需要(不准确,只是大概这样):

#include <gtest/gtest.h> // 当然偷懒也要include头文件,按照基本【哔~】

TEST(XieXing, GanSi) {
  EXPECT_EQ(...);
  EXPECT_NE(...);
  ...
}

// 在Google里,在BUILD文件(类似Make)中链接一个额外的gtest main的库,还能省掉手写main函数,轻松加愉快
int main(int ac, char* av[]) {
    testing::InitGoogleTest(&ac, av);
    return RUN_ALL_TESTS();
} 

GMock

简单说说GMock
有些代码,总会有些复杂的依赖。例如使用了某框架的API来发送RPC请求,读取云上的资源BigTable或者需要连接到某个服务请求一些权限获得一个token等等。
这些逻辑会产生一些IO和网络上的开销,在production中使用无可厚非,但如果每个人写代码跑单元测试都要调用这些逻辑,从而又产生很多没什么实际意义的开销(例如发送一些dummy的RPC请求),这对生产环境的资源会造成一定量的浪费。

例如有个类,叫TableManager,他的功能包括在云端创建一张数据表,获得一些表的属性,支持对表进行查找。一些代码作为client使用了这个类,在云端创建表和存取一些数据。但单元测试代码并不希望真的去读写那些在生产环境里的资源,这时候就需要为TableManager类创建一个对应的Mock类来“绕过”生产环境了。

class TableManager {
  public:
  // 创建一个名为“table_name"的表,成功就返回true。如果table_name不合法,该表已经存在(这个方法已经被调用过了)或者TableManager本身没有在这个方法调用之前获得合法的token,创建表的请求被拒绝,这个方法就会返回false。
  virtual bool CreateTable(const string& table_name);

  private:
  // 这个方法会发送一个RPC请求到服务端,根据some_params的内容获得一个token,这样客户端代码在调用这个类的其他关于操作table的方法的时候,才能获得授权,对table资源进行操作。
  virtual void GetAuthorizeToken(const IDParams& some_params);

  // 如果token串可用(非空)就返回true。但在测试环境下并没有真的去获取token,所以token一定是空的,这样CreateTable无论如何都不会返回true了,所以一会儿要做点tiny work。
  virtual bool IsTokenReady();

  string token_;
};

我们不想真的去访问production(生产环境)里的资源,这时我们就可以先创建一个Mock类:

#include "gmock/gmock.h" // 使用 Google Mock.
class MockTableManager : public TableManager {
    public:
    MOCK_METHOD1(GetAuthorizeToken, string(const IDParams& some_params));
    // 有一个参数的方法
    MOCK_METHOD1(CreateTable, bool(const string& table_name));
    MOCK_METHOD0(IsTokenReady, bool());
    // 0个参数的方法,METHOD0
};

一个使用TableManager的类的方法在准备数据,当数据全部准备之后,就会创建一张新表:

bool ClientClass::ProcessData(const string& src_path, const string& table_name, const string& id) {
    // Prepare data from src_path
    ...
    // When preparation is done, store the data to the table.
    IDParams params;
    params.set_id(id);
    ... // Fill other fields
    table_manager_->CreateTable(table_name);
}

在单元测试里面,测试用例就可以这么写

TEST_F(ClientClassTest, ProcessData) {
    // 测试ClientClass里的ProcessData方法
    // 初始化工作...创建一个mock对象,把该对象的指针传给ClientClass初始化ClientClass.
    MockTableManager mock_table_manager;
    ClientClass client(&mock_table_manager);

    // 正片开始了!我们知道ProcessData方法会调用CreateTable,而CreateTable会先调用GetAuthorizeToken去获得一个token,然后用IsTokenReady来验证一下token是否可用,再做爱做的 事情,嗯嗯。
    EXPECT_CALL(mock_table_manager, GetAuthorization(params)).Times(AtLeast(1));
    // 在接下来的测试里,GetAuthorization至少被调用一次。
    EXPECT_CALL(mock_table_manager, IsTokenReady()).WillRepeatedly(Return(true));
    // 由于并没有真正发rpc请求获取token,token将是空的,但为了测试,经研究决定,就让你“Ready(成为美利坚合众国大总统,咦!?)”。
    EXPECT_CALL(mock_table_manager, CreateTable("valid_table_name")).WillOnce(Return(true)).WillRepeatedly(Return(false));
    // 在这个case里,第一次调用CreateTable肯定是成功的,因为我们从未创建表,操作会成功,当再调用一次就会返回失败,因为理论上这张表已经创建过了。
    // 准备好了,开车!
    EXPECT_TRUE(client_class_obj.ProcessData("dummy_path", "valid_table_name", "1024_good_man_whole_life_safe"));
    // 没问题,这里测试的期望结果是true,因为是第一次创建表,应该会成功。换言之如果测试在这里失败了,就该看看有没有bug了。

    // 假设我们作死再调用一次,这时候应该失败,因为相同更多表已经创建过了。
    EXPECT_FALSE(client_class_obj.ProcessData("dummy_path", "valid_table_name", "1024_good_man_whole_life_safe");
    // 如果实际上ProcessData返回的是true,那就得怀疑一下人生了,因为跟我们想好的不一样啊!说好的“valid_table_name”只能被创建一次呢!?这种bug出现的原因有很多,一些时候可能纯粹是因为手抖都copy了一行CreateTable。
}

Mock了TableManager类之后,我们既能测试代码的功能,又能避免浪费生产环境的资源,是不是有点小愉快?

Google内部有很多功能丰富的开发工具和库,而且文档齐全,经得起岁月的考验,很多优秀的工具和框架都已经开源(例如GTest框架,Protocol Buffers,GRPC等等),实在是居家旅行,杀人越货,开矿推塔,仁义无双,优势很大,吃肉人族,上天入地的必备佳品。

作为一只菜鸡,东西写得不是很好,就做一点微小的工作,分享给大家,谢谢各位 :D

原文地址:https://www.cnblogs.com/wadezhou/p/12091622.html

时间: 2024-11-03 21:04:01

【转】各位都是怎么进行单元测试的?的相关文章

单元测试React

React单元测试——十八般兵器齐上阵,环境构建篇 一个完整.优秀的项目往往离不开单元测试的环节,就 github 上的主流前端项目而言,基本都有相应的单元测试模块. 就 React 的项目来说,一套完整的单元测试能在在后续迭代更新中回归错误时候给与警示,但鉴于 React 本身的特殊性,我们又常常将其与 webpack 等工具相结合,其单元测试的部署相比常规的项目要折腾的多. 本文将作为 React 单元测试系列的开篇,和大家一同逐步构建其单元测试的环境. 你可以在我的仓库下载到本文的示例.

爱上iOS单元测试系列之爱上她就要先了解她:单元测试入门

前言 对于单元测试一开始我是拒绝的.单元测试是一个什么东东,因为我喜欢做iOS开发是因为喜欢写APP的啊,一切和这一目标不相干的东西我没兴趣啊,所以从事iOS开发几年都没去深入学习过单元测试(主要是之前单位没这要求).看到我的优点木有:目标性很强,嗯,记住我的优点,请忽略我拒绝学习边缘知识的缺点.但是最近被总监要求负责单元测试的探索和落地,我义(勉)不(为)容(其)辞(难)地扛起了夺取桥头堡的重任.随着对这个姑娘的不断的了解和接触,我发现自己逐渐爱上了她,她有着独立知性的气质,有着完美丰满的身材

ASP.NET 系列:单元测试

单元测试可以有效的可以在编码.设计.调试到重构等多方面显著提升我们的工作效率和质量.github上可供参考和学习的各种开源项目众多,NopCommerce.Orchard等以及微软的asp.net mvc.entity framework相关多数项目都可以作为学习单元测试的参考..NET单元测试艺术和C#测试驱动开发都是不错的学习资料. 1.单元测试的好处 (1)单元测试帮助设计 单元测试迫使我们从关注实现转向关注接口,编写单元测试的过程就是设计接口的过程,使单元测试通过的过程是我们编写实现的过

Visual Studio 2010 单元测试之一---普通单元测试

原文:Visual Studio 2010 单元测试之一---普通单元测试 本文以Visual Studio 2010为例,来介绍如何在Visual Studio里面进行单元测试. 首先来介绍普通单元测试,这是进行顺序测试.压力测试的基础.如果在Visual Studio 2010(2008)里面没有发现下图中的Test菜单,请用Visual Studio安装光盘进行安装,因为Visual Studio单元测试插件安装时可能不是默认选项. 测试之前,我们要准备一些测试代码.或者从下面的链接下载完

词频统计1.1版——单元测试

前言:网上讲c语言单元测试的内容确实不太多,很多讲单元测试的都注重讲了单元测试的重要性.cppunit.junit的用法,但是没找到具体该如何进行单元测试的帖子,这就导致这几天知道了单元测试很重要,却不知道怎么写好这个测试,网上教程里的例子都是Assert.AreEqual,所以目前的收获很有限,试着在vs里写了几个测试,但是感觉并没有抓住单元测试的精髓,迷惑的很,希望老师多多指点.不过很神奇的一点是,我很深刻的认识到了单元测试的实用性,因为我以前写程序的时候,整体编译运行的时候如果出错了,自己

构建自动化、跨浏览器的 JavaScript 单元测试

我们都知道在多个浏览器中测试我们的代码是多么的重要.至少在我们发布第一个项目的时候,我认为我们在网络开发社区做大部分工作还是相当不错的. 我们做的不够好的工作是测试代码时每一次做出的改变. 我个人对此感到很惭愧.我已经把"学习如何构建自动化.跨浏览器的JavaScript的单元测试"列在我的年度to-do清单中,但我每一次坐下来真正想要做的时候,我又退却了.虽然我肯定这一部分原因是因为我的懒惰,同时我认为这也是由于缺乏良好的可用信息在这个主题上. 有许多工具和框架(例如 Karma)宣

架构之路:性能与单元测试

似乎程序员都是急性子,或许是被windows冗长的开机时间折磨够了,有可能是因为提升性能的效果是最显而易见的……总之,我发现,绝大部分程序员对性能的关注和热情是无与伦比的! C#刚刚推出的时候,就有人摇头晃脑的说,“嗯,自动垃圾回收,性能不行吧?” DataSet横空出世,马上有很多人写代码,在DataSet里插入几百万条数据,证明DataSet的性能问题 Linq当然更要被骂了,尼玛用反射?反射是什么,同学们知道么?性能大老虎呀!更不用说那些自动生成的sql了,有我手写的高效么? …… 所以直

[PYTHON]一个简单的单元测试框架

最近尝试了一下TDD(测试驱动)的模式,感觉效果不错,在此总结一下,同学们如果有更好的办法,一定要告诉我:) 1. 每个功能模块(文件),配一个单元测试模块. 以手头这个项目为例子:有LogCat.py, LogModel.py, SceneBuilder.py 三个模块,那么就相应的新建LogCatTest.py, LogModelTest,SceneBuilderTest.py三个文件 2. 每个函数都相应写一个单元测试例. 比如:在LogCat.py里有三个函数: def parseDat

Android系列----JUnit单元测试的使用

?[声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/4020443.html 联系方式:[email protected] [正文] 一.单元测试的引入: 如果只是普通的一个小程序,编写测试是有些多此一举,但是当项目比较庞大的时候,一般都应该去编写单元测试.JUnit测试是白盒测试,即主要是程序员自己对开发的方法进行功能性测试.JUnit是一套框架,