代码质量(2): 圈复杂度和可测试性
- 最好用的C++圈复杂度分析工具:pip install lizard,没想到它解析C++的函数块超级快,可以用来作为建立进一步的代码片分析的基础,完胜其他所有工具。
- 我花了1天多将一个C++模块测试覆盖率做到100%,对于代码如何才具有良好可测试性有了直观的经验,从测试的角度看代码的设计是否简洁是一个非常合理的角度。
- 如果代码的可测试性很好,那么AI也能非常容易的处理它,可见未来的代码,不是要写的复杂,就是写的直接简洁,实际上就是要保持线性。
圈复杂度计算
圈复杂度的计算公式:
CC = 分支判断的数量 + 1
对于分支判断表示每次出现以下的:
-'if'
-'for'
-'while'
-'do-while'
-'case'
-'catch'
-条件表达式 'a?b:c'
-逻辑运算符 '&&' 和 '||'
例子
void foo(int a, int b){
switch (a){
case 1: // 1
break;
case 2: // 2
break;
case 3: // 3
break;
default:
break;
}
if(a||b){ // 4, 5
}
if((a||b)&&(a&&b)){ // 6, 7, 8, 9
}
do{ // 10
}while(0);
}
如何将一个难以测试的类改进为易于测试的类?
例子1,切分辅助的纯函数
让这个类的构造依赖的参数简单,简单不一定是参数个数少,而是这些参数的构造本身的依赖不要有太多深度。
以前有一个痛点问题是,一个类的内部有一些成员变量不好修改,于是不好再测试的时候控制函数内部的分支行为。解决方式是:将控制复杂行为的函数实现拆分,拆分为调用几个辅助函数,这几个辅助函数是纯函数,依赖的参数都通过函数参数传递(即使这个参数是类成员变量就有的),这样这个辅助函数就容易被测试,因为是一个纯函数,因为都通过函数参数传递状态。
例子:
class A{
public:
void run(){
if(config.enable_xxx){
do_something();
}
}
void dosomthing(){
status.value = ...
}
private:
Config config;
Status status;
}
这个run就不好测试,因为测试方不好控制Config的状态。修改如下:
class A{
public:
void run(){
do_something(config, status);
}
bool do_somthting(const Config& config, const Status& status){
if(!config.enable_xxx){
return false;
}
status.value = ...
}
private:
Config config;
Status status;
}
这个时候就可以对 do_something 做测试了,因为do_somthting依赖的对象,和修改的对象,都是通过函数参数传递的,是一个纯函数。那么测试函数和A::run 对于do_somthting来说都是同等的访问能力。
例子2: 处理内部的返回值问题
下面的类,内部对于某个API请求对结果做处理,但是测试方不易于控制API请求的结果,不好测试到对应分支。
class A{
public:
void run(){
xxx v;
bool ret = receiver.pop(v);
if(ret){
....
}
}
}
解决方式是把获取数据和处理数据完全的分开成两个部分:
class A{
public:
void run(){
xxx v;
bool ret = receiver.pop(v);
on_receive_v(ret, v);
}
bool on_receive_v(bool ret, const xxx& v){
if(!ret){
return false;
}
...
}
}
这样分支跑到 on_receive_v 里面去,而 on_receive_v 是一个易于通过参数控制分支测试的函数。
这个问题的本质是:代码如何做依赖倒置(Dependency Inverse),依赖注入(Dependency Inject)。很多教代码依赖倒置,依赖注入的教程从这个角度来说都教错了,应该从测试的角度来教。从测试的角度来说,如果不做依赖倒置和依赖注入设计,那么这个代码的可测试性就很差,因为它依赖的决定了代码内部分支走向的状态,外部无法便利的动态“配置”。
面向对象方面:如果一个类 A 内部直接创建了 B的实例子,那么你要在测试的时候动态调整A内部的B实例的状态,就非常困难。所以,首先要让B是通过 A的构造函数,或者A的set接口传入的,这样测试的时候B的实例你才有机会动态传入。其次,A内部用到的B的接口,你就应该设计成 virtual 方法,否则的话 B 类的这些方法的行为可能很难按需调整,如果是 virtual 方法,你可以轻易做一个mock的子类B实现,测试的时候使用你的MockB的实例。
函数式方面:策略依赖的函数,可以把函数指针作为参数。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix