编写优秀的单元测试(六)编写可读的测试
在上一篇文章我们讲了如何编写一个测试,这一章我们讨论一下如何让我们的测试变得可读性高一点。
什么代码不好读
网上有一个段子,说程序员最烦做的两件事情,一个是写文档,一个是写注释。程序员最烦别人不做的两件事情,一个是不写文档,一个是不写注释。这里提到了程序开发里面的一个最基本的事实:
读代码比写代码难。
这也是我们经常在工作中重构的原因,因为我们经常觉得有看那些代码的时间,我都重写一遍了。
说这些是为了告诉我们,要让写的测试代码好读,它需要比我们认为的简单代码更简单,更傻瓜,更清晰。
导致代码难读的原因有很多,我们这里讨论几种让测试代码难以阅读的原因,在后面我们逐个的解释这些原因:
- 抽象层次过低,断言过于基本
- 过度断言
- 附加细节
- 体现多个测试
- 测试方法过于冗长或过于分散
- 魔法数字
- 安装方法过于冗长
断言过于基本(使用基本断言)
断言过于基本的意思是:大量使用基本断言,一眼看不出来在断言些什么。我们看下面的例子:
public class BaseAssertTest
{
[Fact]
public void OutTest()
{
A a = new A();
Assert.True(a.Out("test").IndexOf("te") != -1);
}
}
public class A
{
public string Out(string s)
{
return s;
}
}
我们写一个很简单的测试程序用于说明一下这个场景,上面的例子中,我们的断言一眼看过去不太好理解,原因是这段代码并不符合我们的阅读习惯。如果有人觉着这个代码还行,能读,那我们看看修改之后的代码
[Fact]
public void OutTest()
{
A a = new A();
Assert.Contains("te", a.Out("test"));
}
看到这个代码,我们立马可以读出来,这个断言是看test放到a的Out方法输出出来的结果是不是包含了te字符串,可读性立马就强了很多。
经过上面的例子我们发现,要写出有意义的,容易读的断言,需要我们:
- 提升抽象层次,根据要断言的实际意义提升代码的抽象层次,使之可读性增强
- 合理的使用语言特性,单元测试库给我们提供的方法,深入了解工具能让我们用更好的方式表达我们的测试意图。
过度断言
如果过度断言,会让我们的系统中出现很多不明所以的断言,同时我们在运行测试的时候会发现,经常被测试打断。
举个例子,如果一个类用于从数据库中拿出所有人员的考勤信息,对比处理,最后输出迟到人员的名单,程序员A为了省事,直接从数据库中拿出了一千名员工的打卡信息,然后分离出迟到人员的名单,构建输出字符串,指定断言:
输入总数据 - 运行 - 断言输出结果是不是等于构建的输出结果
这样做确实能达到测试的目的,但是,系统中一些微小的变化就会让断言失败,比如,新加了一条需求,出差人员不打卡不算迟到,这时候我们就需要修改源数据,期望数据,以求测试通过,而在修改之前,测试就会一直通不过。我们举的例子还是比较简单的,试想,在一个系统中修改了一小段代码,一堆测试都无法通过,是多么绝望。如果这些不通过的测试是有意义的,那正是我们编写测试的意义,如果这些测试都是无意义的,那真是让人头大。
上面的例子告诉我们,要尽量的少些一些全量的测试,不要输入一大串,输出一大串,而是针对目标的进行测试。
所以我们所说的过度断言不仅是指应该去掉无意义的断言,减少断言数量,更重要的是讲应该提升断言质量。
如何提升断言质量,就我们之前的例子来说,我们可以做下面的思考:
- 一千条数据在不做压力测试的时候有必要吗,如果没有必要,理论上两条数据(一个迟到 一个不迟到)就能测试出这个逻辑是不是生效了
- 能否拆分?如果包含多种case,出差的,迟到的,没迟到的,请假的,外派无固定办公室人员,高级管理人员等,我们可以针对不同的情况,编写多条测试,将一个大而全的测试拆分成多个小的测试,这样,当业务改变,我们就相应的修改测试,当业务没有改变,如果修改代码导致测试报错,我们就能很快的定位是哪里错了,这样才达到了测试的目的。
附加细节
附加细节是指测试中不能一目了然的部分,这部分的问题是:抽象过于复杂,以至于隐藏了真实的意思,使人不能一目了然的读出测试的意思。出现这个问题的原因一般是抽象层次过于复杂。(我们之前讲了 基本断言 引起的原因很多时候是没有抽象,这里我们又讲了,抽象层次不能过于复杂,那么抽象层次如何为好呢,我们说,一目了然就好,单一的抽象层次就好)
为了将测试的附加细节去除掉,我们需要:
- 将一些不需要关注的环境构建,对象准备的代码抽离到私有的方法,或明确标注安装部分
- 命名需要恰当和具有描述性
- 一个方法尽量只有一个抽象层次
体现多个测试
如果一个测试,测试了多个问题,这是不好的,一个测试只检查一件事,不要胃口太大,这个注意和过度断言区分。
过度断言指的是我们要断言的事情太大,颗粒度太大,一旦发生错误也不好准确的判断错误位置,体现多个测试是指我们在一个测试中断言了多个完全不同的问题。
比如:一个测试方法先断言了传入参数是否合法,再断言了部分逻辑,而后又断言了返回的参数是否符合预期,这是不好的。
一个测试应当只关注一件事情。
测试方法过于冗长或过于分散
这个也很好理解,过于冗长或过于分散(分散在多个文件中,变量定义在专门的变量定义文件中,测试时直接使用,不好查找)的代码编写方式都会降低我们的可读性,在这里我们建议:
- 短小的变量定义,环境准备,直接内联到测试方法中
- 较长较复杂的环境准备代码,使用工厂方法,或者构造器,将其隐藏在私有方法中(这个方法的使用与我们在 附加细节 中的方法异曲同工)
- 如果实在没办法再将其拆分到单独的文件中
- 与团队协商使用一致的构造密度及方法(如,资源放到同一个地方,在同一个文件夹中构造环境,变量的定义放到同一个文件中等)
魔法数字
这个意思不用解释,解决方法也很常见:用有意义变量名的变量替换魔法数字。我们在实际的使用中:
- 如果该变量在很多测试都用过且变量值不变:直接定义常量
- 如果该变量只有该测试使用:直接定义私有变量
我们这里着重介绍有一种减少私有变量,还可以明确变量意义的方法:
public void perfectGame()
{
Roll(Pins(10), Times(12));
Assert.Equal(game.Score(), Score(300));
}
private int Pins(int n) => n;
private int Times(int n) => n;
private int Score(int n) => n;
使用私有方法封装魔法数字,即保证了可读性,也提升了变量使用的灵活性。
安装方法过于冗长
我们之前讲了如果测试方法过于冗长需要将安装方法抽出,我们还需要注意,抽出的安装方法如果还是过长,依然需要再次进行抽离和抽象,不要编写过于冗长的安装方法,我们可以尝试:
- 抽取过于细节的代码放入私有的方法
- 命名的时候特别小心,给与适当的,描述性的命名
- 在安装中的代码尽量做到属于同一抽象层次
前两条可能比较好理解,最后一条,“同一抽象层次”如何理解呢?简单说 “同一抽象层次” 就是指导我们提出私有方法,这里我讲一个我在工作中常用的方法,也是编程中常用的方法:由外而内,逐步细化,强化设计,弱化细节。
我举一个例子说一下:根据业务需要,我们需要排序展示目前系统中所有设备的名称,而设备名称来自多个源,这时候我先不关注实现细节,先把业务流程写出来(这实际上每个程序员拿到需求都会做,我们需要强化这种方法,并且关注其产出):
- 从数据库取出设备名
- 从特定文件中取出用户自定义的设备名
- 从网络中获取在线的设备名
- 进行去重
- 进行排序
写出了流程之后,这就是第一抽象,我们在BLL层的代码就是这样的:
public void ShowTerminal()
{
var A = GetDataFromDB();
var B = GetDataFromFile();
var C = GetDataFromNet();
var D = RemoveDuplicate(A, B, C);
var ret = Sort(D);
}
这个流程就是第一抽象,业务流程抽象,下面还有几层抽象取决于业务复杂度,比如 有好几种数据库的存储,我们还需要抽象一层来组装数据库回来的数据。
在使用这种方法的时候,我们需要注意要经常检查,由于业务和技术的差异性,一旦发现当前的抽象层级设计有误需要及时调整。
上一篇: Python查找相似单词的方法
推荐阅读
-
用仿ActionScript的语法来编写html5——第六篇,TextField与输入框
-
编写Go程序对Nginx服务器进行性能测试的方法
-
教你如何编写Vue.js的单元测试的方法
-
微信小程序授权 获取用户的openid和session_key【后端使用java语言编写】,我写的是get方式,目的是测试能否获取到微信服务器中的数据,后期我会写上post请求方式。
-
编写单元测试的良好准则
-
Android编程入门-单元测试代码的编写
-
如何才能编写出一套优秀的软文营销策划方案
-
读书笔记---《编写可读代码的艺术》
-
.NET Core TDD 前传: 编写易于测试的代码 -- 构建对象
-
.NET Core TDD 前传: 编写易于测试的代码 -- 缝