欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

编写优秀的单元测试(六)编写可读的测试

程序员文章站 2022-04-26 09:14:30
...

在上一篇文章我们讲了如何编写一个测试,这一章我们讨论一下如何让我们的测试变得可读性高一点。

什么代码不好读

网上有一个段子,说程序员最烦做的两件事情,一个是写文档,一个是写注释。程序员最烦别人不做的两件事情,一个是不写文档,一个是不写注释。这里提到了程序开发里面的一个最基本的事实:

读代码比写代码难。

这也是我们经常在工作中重构的原因,因为我们经常觉得有看那些代码的时间,我都重写一遍了。

说这些是为了告诉我们,要让写的测试代码好读,它需要比我们认为的简单代码更简单,更傻瓜,更清晰。

导致代码难读的原因有很多,我们这里讨论几种让测试代码难以阅读的原因,在后面我们逐个的解释这些原因:

  1. 抽象层次过低,断言过于基本
  2. 过度断言
  3. 附加细节
  4. 体现多个测试
  5. 测试方法过于冗长或过于分散
  6. 魔法数字
  7. 安装方法过于冗长

断言过于基本(使用基本断言)

断言过于基本的意思是:大量使用基本断言,一眼看不出来在断言些什么。我们看下面的例子:

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字符串,可读性立马就强了很多。

经过上面的例子我们发现,要写出有意义的,容易读的断言,需要我们:

  1. 提升抽象层次,根据要断言的实际意义提升代码的抽象层次,使之可读性增强
  2. 合理的使用语言特性,单元测试库给我们提供的方法,深入了解工具能让我们用更好的方式表达我们的测试意图。

过度断言

如果过度断言,会让我们的系统中出现很多不明所以的断言,同时我们在运行测试的时候会发现,经常被测试打断。

举个例子,如果一个类用于从数据库中拿出所有人员的考勤信息,对比处理,最后输出迟到人员的名单,程序员A为了省事,直接从数据库中拿出了一千名员工的打卡信息,然后分离出迟到人员的名单,构建输出字符串,指定断言:

输入总数据 - 运行 - 断言输出结果是不是等于构建的输出结果

这样做确实能达到测试的目的,但是,系统中一些微小的变化就会让断言失败,比如,新加了一条需求,出差人员不打卡不算迟到,这时候我们就需要修改源数据,期望数据,以求测试通过,而在修改之前,测试就会一直通不过。我们举的例子还是比较简单的,试想,在一个系统中修改了一小段代码,一堆测试都无法通过,是多么绝望。如果这些不通过的测试是有意义的,那正是我们编写测试的意义,如果这些测试都是无意义的,那真是让人头大。

上面的例子告诉我们,要尽量的少些一些全量的测试,不要输入一大串,输出一大串,而是针对目标的进行测试。

所以我们所说的过度断言不仅是指应该去掉无意义的断言,减少断言数量,更重要的是讲应该提升断言质量。

如何提升断言质量,就我们之前的例子来说,我们可以做下面的思考:

  • 一千条数据在不做压力测试的时候有必要吗,如果没有必要,理论上两条数据(一个迟到 一个不迟到)就能测试出这个逻辑是不是生效了
  • 能否拆分?如果包含多种case,出差的,迟到的,没迟到的,请假的,外派无固定办公室人员,高级管理人员等,我们可以针对不同的情况,编写多条测试,将一个大而全的测试拆分成多个小的测试,这样,当业务改变,我们就相应的修改测试,当业务没有改变,如果修改代码导致测试报错,我们就能很快的定位是哪里错了,这样才达到了测试的目的。

附加细节

附加细节是指测试中不能一目了然的部分,这部分的问题是:抽象过于复杂,以至于隐藏了真实的意思,使人不能一目了然的读出测试的意思。出现这个问题的原因一般是抽象层次过于复杂。(我们之前讲了 基本断言 引起的原因很多时候是没有抽象,这里我们又讲了,抽象层次不能过于复杂,那么抽象层次如何为好呢,我们说,一目了然就好,单一的抽象层次就好)

为了将测试的附加细节去除掉,我们需要:

  1. 将一些不需要关注的环境构建,对象准备的代码抽离到私有的方法,或明确标注安装部分
  2. 命名需要恰当和具有描述性
  3. 一个方法尽量只有一个抽象层次

体现多个测试

如果一个测试,测试了多个问题,这是不好的,一个测试只检查一件事,不要胃口太大,这个注意和过度断言区分。

过度断言指的是我们要断言的事情太大,颗粒度太大,一旦发生错误也不好准确的判断错误位置,体现多个测试是指我们在一个测试中断言了多个完全不同的问题。

比如:一个测试方法先断言了传入参数是否合法,再断言了部分逻辑,而后又断言了返回的参数是否符合预期,这是不好的。

一个测试应当只关注一件事情。

测试方法过于冗长或过于分散

这个也很好理解,过于冗长或过于分散(分散在多个文件中,变量定义在专门的变量定义文件中,测试时直接使用,不好查找)的代码编写方式都会降低我们的可读性,在这里我们建议:

  1. 短小的变量定义,环境准备,直接内联到测试方法中
  2. 较长较复杂的环境准备代码,使用工厂方法,或者构造器,将其隐藏在私有方法中(这个方法的使用与我们在 附加细节 中的方法异曲同工)
  3. 如果实在没办法再将其拆分到单独的文件中
  4. 与团队协商使用一致的构造密度及方法(如,资源放到同一个地方,在同一个文件夹中构造环境,变量的定义放到同一个文件中等)

魔法数字

这个意思不用解释,解决方法也很常见:用有意义变量名的变量替换魔法数字。我们在实际的使用中:

  1. 如果该变量在很多测试都用过且变量值不变:直接定义常量
  2. 如果该变量只有该测试使用:直接定义私有变量

我们这里着重介绍有一种减少私有变量,还可以明确变量意义的方法:

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;

使用私有方法封装魔法数字,即保证了可读性,也提升了变量使用的灵活性。

安装方法过于冗长

我们之前讲了如果测试方法过于冗长需要将安装方法抽出,我们还需要注意,抽出的安装方法如果还是过长,依然需要再次进行抽离和抽象,不要编写过于冗长的安装方法,我们可以尝试:

  1. 抽取过于细节的代码放入私有的方法
  2. 命名的时候特别小心,给与适当的,描述性的命名
  3. 在安装中的代码尽量做到属于同一抽象层次

前两条可能比较好理解,最后一条,“同一抽象层次”如何理解呢?简单说 “同一抽象层次” 就是指导我们提出私有方法,这里我讲一个我在工作中常用的方法,也是编程中常用的方法:由外而内,逐步细化,强化设计,弱化细节。

我举一个例子说一下:根据业务需要,我们需要排序展示目前系统中所有设备的名称,而设备名称来自多个源,这时候我先不关注实现细节,先把业务流程写出来(这实际上每个程序员拿到需求都会做,我们需要强化这种方法,并且关注其产出):

  1. 从数据库取出设备名
  2. 从特定文件中取出用户自定义的设备名
  3. 从网络中获取在线的设备名
  4. 进行去重
  5. 进行排序

写出了流程之后,这就是第一抽象,我们在BLL层的代码就是这样的:

public void ShowTerminal()
{
    var A = GetDataFromDB();
    var B = GetDataFromFile();
    var C = GetDataFromNet();
    var D = RemoveDuplicate(A, B, C);
    var ret = Sort(D);
}

这个流程就是第一抽象,业务流程抽象,下面还有几层抽象取决于业务复杂度,比如 有好几种数据库的存储,我们还需要抽象一层来组装数据库回来的数据。

在使用这种方法的时候,我们需要注意要经常检查,由于业务和技术的差异性,一旦发现当前的抽象层级设计有误需要及时调整。