简介:咱们团队在手淘中次要负责 BehaviX 模块,代码次要是一些逻辑性能,很少波及到 UI,为了缩小双端不统一问题、进步性能,咱们采纳了将外围代码 C ++ 化的策略。因为团队我的项目偏底层,测试同学难以齐全笼罩,回归老本较高,局部性能依赖研发同学自测,为了进步零碎的稳定性,咱们在团队中履行了单元测试,同时因为团体客户端 C ++ 单元测试相干教训积淀较少,所以在此分享下团队在做单元测试中遇到的问题与解决思路,心愿能对大家所有帮忙。
作者 | 思兼起源 | 阿里开发者公众号背景咱们团队在手淘中次要负责 BehaviX 模块,代码次要是一些逻辑性能,很少波及到 UI,为了缩小双端不统一问题、进步性能,咱们采纳了将外围代码 C ++ 化的策略。因为团队我的项目偏底层,测试同学难以齐全笼罩,回归老本较高,局部性能依赖研发同学自测,为了进步零碎的稳定性,咱们在团队中履行了单元测试,同时因为团体客户端 C ++ 单元测试相干教训积淀较少,所以在此分享下团队在做单元测试中遇到的问题与解决思路,心愿能对大家所有帮忙。为什么要应用单元测试 1、运行快如果由测试同学手工测试,可能测试周期很长,对于性能比较复杂的性能,测试同学可能并不能残缺笼罩所有预期链路,也可能因为某些操作而错过一些关键性步骤。2、缩小回归老本应用单元测试,能够在每次批改代码后从新运行整套测试,尽可能保障新代码不会毁坏现有性能。3、优化代码构造当代码耦合度十分大时,可能很难进行单元测试。为代码编写测试将天然地依照预期性能拆散你的类。单测工程搭建历程单测环境搭建运行环境的抉择 C ++ 工程因为一些三方库的依赖(须要筹备多个平台的链接库),同一份代码想要在不同操作系统上运行略微有点艰难。为了可能让单测工程疾速运行起来,同时也不便开发同学调试,兼顾 Android/iOS 同学的开发习惯,在运行环境上反对单测反对在 MacOS 和 Linux 下运行。依赖剥除因为单测环境是运行在电脑环境的,所以必须要把一些内部依赖去除。Java/OC 的 API 依赖波及到跨语言通信时,通过 NativeBridge 封装,外部通过宏或 cpp 文件链接辨别 Android 和 iOS 环境
内部库的依赖个别采取源码依赖或打出多平台链接库(须要 MacOS 和 Linux 版本的依赖)的依赖形式解决。单测框架目前业内 C ++ 支流单测框架为 google 的 gtest + gmock。gtest 提供了一些单元测试中的断言工具,gmock 提供了一些 mock 性能,然而性能比拟弱。MOCK 工具 gtest 提供的 gmock 工具性能比拟弱,只能通过继承的形式 mock 虚函数,对于 C ++ 来说是极其不不便的。在 Java 中,成员办法是默认能够被派生类重写的,java 支流 mock 工具 mockito 正是利用了这一个性来实现 mock 操作。在 C ++ 中,所有函数默认是不能被重写的,而且存在一些动态函数和工具函数,无奈通过继承重写的形式实现 mock。最终咱们基于开源的 hook 工具 frida 进行封装,实现了本人的 mock 工具。
部署到服务器运行依赖装置为了使单测工程和其余零碎买通(如:钉钉群、Aone),单测工程同时也反对在 Linux 环境中运行。因为 C ++ 语言的特殊性,从本机环境(MacOS)迁徙到 Linux 并不是一帆风顺的。团体的服务端机器应用的是 CentOS,而且只能下载内网环境中已有的软件,版本也比拟老,而且团体机器对 C ++ 的环境反对稍弱,如:编译器不反对 C ++11 语法,CMake 版本低,没有 Clang 编译器等。所以大部分依赖咱们都是通过源码的模式导入到服务端机器中,编译出可执行文件装置。生成镜像(可选)在编译器、CMake 等工具装置好了之后,能够为以后环境创立 docker 镜像,这样下次就能部署到其余机器间接应用了。外围性能建设覆盖率单测代码覆盖率通过减少编译参数 -fprofile-arcs 和 -ftest-coverage,在编译实现后每个源文件会生成对应的.gcno 文件,在程序运行完结时会生成.gcda 文件,而后能够在单元测试运行实现后,应用 lcov/gcov,统计代码运行的覆盖率。留神,举荐应用动静链接的形式将你的待测工程库链接到每个测试用例中,如果应用动态链接,在单元测试运行实现后可能会有一些没有被任何用例笼罩到的文件没有生成.gcda 文件,在计算代码覆盖率时这些源文件会被脱漏。增量代码覆盖率应用 git merge-base 能够获取两次提交最佳的公共先人。
拿到最佳公共先人与以后节点的提交记录,通过 git diff 和 git blame,就能够取得两次提交的增量代码行,联合代码覆盖率能够计算出增量代码覆盖率。内存透露查看 C ++ 代码很容易写出内存透露,所以咱们在单测工程中集成了 valgrind 工具,能无效的检测出内存透露的代码。上面是一个简略的示例
钉钉群播报每次代码合并到 develop 分支的时候,钉钉群中会播报本次测试的通过率以及代码覆盖率与上次合并时时差值等信息,不便大家及时修复问题,通过覆盖率增长差值也能够调动团队写单测的积极性。code review 卡口在提交 code review 时,大家能够看到本次代码的单测通过率、单测覆盖率、增量覆盖率等信息,如果单元测试运行没有通过,或增量覆盖率卡口未通过(目前团队中要求增量单测覆盖率达到 90%),则不容许合并代码。
单元测试实际如何编写无效的单元测试用例单元测试的组成部分个别单元测试由以下几局部组成测试数据:尽可能稳固,缩小对不确定性因素的依赖逻辑执行体:要明确以后测试用例测试的是哪个函数、哪个分支逻辑,不要一次性笼罩大多后果校验:尽可能残缺,不要只校验函数返回值单元测试的准则单元测试必须遵循的准则:独立性:单元测试是独立的,能够独自运行,并且不依赖于任何内部因素,如文件系统或数据库。幂等性:每次运行单元测试应与其后果统一,测试中不要依赖如工夫、日期等不确定因素疾速:不要依赖网络申请等耗时操作教训小结编写单元测试时倡议从以下角度思考实现什么性能,解决哪些数据,最终输入什么?异样和边界在哪里?函数的要害后果是否都验证到?蕴含返回值和两头值。函数的危险在哪里,哪局部逻辑不太自信,最容易出错?并不是所有函数都须要单测,如 get/set 等逻辑比较简单的的,不肯定须要写。进步代码的可测试性 C ++ 是一门多范式的语言,而且因为 C + 语言自身的一些个性(RAII,模板等),网上很多基于 Java 等语言总结进去的进步可测试性的办法对 C ++ 来说可能过于麻烦,如依赖注入等,不肯定特地实用。上面整顿了一些简略罕用能进步可测试性的形式。影响可测试性的常见因素内部依赖过多,须要 mock 数据依赖链过长,导致结构测试数据麻烦分支逻辑过于简单全局变量 / 动态变量外部 lambda 表达式过多依赖的类对象不可结构 / 难以构造函数性能过多缩小全局变量 / 动态变量的应用如果你的对象依赖了一些全局变量 / 动态变量,而且这些全局变量会在多个测试 case 应用,这种状况是比拟难测试的,你不得不在每个测试用例完结之后手动重置全局变量。这样不合乎单测测试的独立性准则,所以应该尽量避免应用全局变量。class MyTest {
public:
int GetIndex() {return index++;}
static int index; // 动态变量
};
int MyTest::index = 0;
TEST(test, demo) {
ASSERT_EQ(0, MyTest().GetIndex());
}
TEST(test, demo2) {
ASSERT_EQ(0, MyTest().GetIndex()); //Error
}
TEST(test, demo) {
MyTest::index = 0;
ASSERT_EQ(0, MyTest().GetIndex());
}TEST(test, demo2) {
MyTest::index = 0;
ASSERT_EQ(0, MyTest().GetIndex());
} 迪米特法令 1、如果你代码中引入一些简单的内部依赖,能够思考将依赖转移给调用方如:class MyClass {
public:
void doSomething() {if(getUserManager().getUser(123).getProfile().isAdmin()) { //bad 简单的依赖链
//xxxx
} else {}}
};class MyClass {
public:
void doSomething(bool isAdmin) { // 简略的参数依赖
if(isAdmin) {//xxxx} else {}}
};2、间接依赖须要的参数,防止依赖相似于 Context 大而全的参数(可能十分难以结构)如:class MyClass {
public:
void processOrderBefore(const UserContext & userContext) { // 批改之前
const User & user = userContext.getUser();
const PlanLevel & level = userContext.getLevel();
const Order & order = userContext.getOrder();
// ... process
}
void processOrderAfter(const UserContext & userContext) { // 批改后
const User & user = userContext.getUser();
const PlanLevel & level = userContext.getLevel();
const Order & order = userContext.getOrder();
processOrderAfter(user, level, order); // 外围逻辑抽成新的函数
}
void processOrderAfter(const User & user, const PlanLevel & level,const Order & order) {
// 只须要对新封装函数进行单元测试即可
// ... process
}
}; 封装分支逻辑如果一个函数中分支太多,能够思考将不同分支封装成不同的函数解决,而后对封装的函数别离编写单元测试用例。正当应用 MOCK 工具思考在以下场景应用 mock 工具,能够缩小你的单元测试老本代码中依赖的某个性能在你本次测试并不关怀,如:db 数据读取,发申请测试用例依赖一些简单的数据源,如:db 数据读取,流水线上游数据,网络申请一些非幂等性的函数调用或者后果返回不稳固的函数调用,如:随机数获取,工夫获取,db 写入对象的某些状态难以创立或者重现,如:网络谬误或者文件读写谬误验证一些两头过程值,如:你的函数没有返回值,或者两头过程值不不便验证,能够 mock 两头某个函数调用来验证两头过程后果是否正确尝试测试驱动开发(TDD)如果你的需要所要实现的性能绝对明确,那么能够先把接口定义进去,写一个最简略的实现运行起来,为其补充单元测试用例,而后再一步步欠缺具体实现细节。如果不能先写测试用例也没关系,重要的是在开发中尽早编写测试测试,不要将它们提早到最初,这样能够及时重构你的代码。
常见误区只测试失常数据该当尽量补充一些非凡值(如空值、边界值)或异样数据,以校验指标函数在不同的输出是否合乎预期,尽量笼罩多的代码分支逻辑。后果校验不残缺如果你的指标测试函数中对属性进行了批改,那么应该尽可能校验这些批改是否合乎预期,而不是单单只校验函数返回值。输出数据过于简单生成测试输出数据的代码该当防止与理论工程代码耦合,如:读取 db 或从流水线上游产生等应用最小数据依赖的准则,只输出对以后测试用例会产生影响的数据即可。如果数据源结构过于简单,能够将一个大的测试用例拆分成多个小的测试用例。测试代码存在分支条件防止测试用例代码中应用 if、switch 等分支逻辑,放弃用例尽量简略,如果须要测试不同分支的代码逻辑,应该拆分成多个测试用例。保护测试用例重构代码时,应该同步批改测试用例发现新增 Bug 时,该当将能验证此 Bug 被修复的测试用例的补充到单元测试工程中测试用例命名规定参考 TEST_F(TestUCPPipelineCenter, checkTaskInProcess_反复触发_true);
测试宏 被测试类名,被测试函数名_简略形容外围测试逻辑_要校验的后果值小结咱们小组的单元测试工程曾经稳固运行了一段时间,代码提交流程也逐渐固化下来了,如下图所示。后续咱们会寻找一些指标去量化掂量单元测试所带来的收益。心愿本文能帮忙大家更加快捷地搭建 C ++ 单元测试环境。
附录「单元测试最佳实际」https://www.jianshu.com/p/641…「从头到脚说单测——谈无效的单元测试(下篇)」http://testerhome.com/topics/…「Frida – Anatomy of a code tracer」https://medium.com/@oleavr/an… 重磅来袭!2022 上半年阿里云社区最热电子书榜单!千万浏览量、百万下载量、上百本电子书,近 200 位阿里专家参加编写。多元化抉择、全畛域笼罩,汇聚阿里巴巴技术实际精髓,读、学、练一键三连。开发者藏经阁,开发者的工作伴侣~ 点击这里,查看详情。原文链接:https://click.aliyun.com/m/10… 本文为阿里云原创内容,未经容许不得转载。