圈复杂度和可测试性
- 最好用的C++圈复杂度分析工具:pip install lizard,没想到它解析C++的函数块超级快,可以用来作为建立进一步的代码片分析的基础,完胜其他所有工具。
- 我花了1天多将一个C++模块测试覆盖率做到100%,对于代码如何才具有良好可测试性有了直观的经验,从测试的角度看代码的设计是否简洁是一个非常合理的角度。
- 如果代码的可测试性很好,那么AI也能非常容易的处理它,可见未来的代码,不是要写的复杂,就是写的直接简洁,实际上就是要保持线性。
如何将一个难以测试的类改进为易于测试的类?
例子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的实例。
函数式方面:策略依赖的函数,可以把函数指针作为参数。