Fixture是软件测试中的一个重要概念。许多人对这个概念感到模糊,原因之一是迄今为止它没有统一的中文翻译。有人叫它测试固件,有人叫它测试装置,还有人叫它测试夹具。我个人没有特别倾向的翻译,因此在文中直接称之为Fixture。
那么,到底什么是Fixture呢?实际上,Fixture概念源于电子学,指的是测试电子元器件所需的辅助装置。后来这个概念被引入到软件测试领域,意指能够提供软件测试所依赖的系统,环境,状态等前置条件的东西。
在我看来,Fixture是一个抽象事物,其抽象性体现在两方面。
一方面,Fixture的功能是抽象的。测试用例的前置条件千变万化,Fixture的具体功能也随之千变万化。例如,一个读取文件内容的测试用例,其前置条件是一个打开的文件,此时Fixture的功能是打开文件;而一个测试HTTP接口的测试用例,其前置条件是一对运行的HTTP服务器/客户端,此时Fixture的功能是创建并启动HTTP服务器/客户端。
另一方面,Fixture的形态也是抽象的。即使是同一个测试用例,在不同测试环境下,Fixture可能存在不同的形态。例如,针对读取文件内容的测试用例,Fixture的作用是打开文件,但是其具体的实现方式有多种可能。既可以是一个双击文件图标的操作,也可以是一行Linux命令,还可以是一段Python脚本。
由于Fixture在功能和形态两方面都是抽象的,因此也不难理解为什么许多人对Fixture有一种似懂非懂的感觉。这里,根据上面的讨论,我们给Fixture下一个自己的定义: Fixture是在软件测试过程中,为测试用例创建其所依赖的前置条件的操作或脚本。
需要补充的是,"凡事有始就有终",Fixture也一样。在执行测试用例前需要准备环境,那么在用例执行之后还需要恢复环境。因此,Fixture还有一个隐含的功能,那就是在测试结束后,恢复测试环境,回归初始状态。
聊到这里,大家可能还是对Fixture的意义不理解。准备和销毁环境,初始化和清除状态,为什么不直接由测试用例自己来完成?"无中生有"弄出一个Fixture,会不会把简单问题复杂化?
这是好问题。这些操作当然能由测试用例自己来完成。然而,"能不能"不是关键,关键是"好不好"。
软件测试有个特点,那就是针对同一用户场景,往往会设计多个前置条件相同但是测试步骤有差异的测试用例(测试套件Test Suite的概念即源于此)。此时,如果每个用例都各自维护前置条件,不仅存在浪费,而且难以保持一致性。
一种更好的做法是"合并同类项"。当多个用例具有相同的前置条件时,我们可以将前置条件提取出来,作为Fixture,由各个用例共同调用。这种做法效率高,易于维护,一致性也更好。
从设计模式角度来看,这种做法遵守了两大原则。一是"单一数据源"(Single Source of Truth)原则,即完成某一功能的Fixture有且仅有一份;二是"关注点分离"(Separation of Concerns)原则,即让Fixture负责测试环境和状态的准备与销毁工作,让测试用例负责测试主体工作,双方各司其职,职责明细。
至此,我们从理论方面介绍了Fixture的原理和含义。接下来,我们从实践方面看看,Fixture是如何应用于工程实际的。
一种最普遍的实现Fixture的方式是Setup/Teardown。它广泛存在于各种主流的自动化测试框架中,例如C++ Google Test, Java JUnit, Python Unittest, Robot Framework等。
它们共同的特点是基于Setup函数或方法,完成对测试环境和状态的准备工作;基于Teardown函数或方法,完成对测试环境和状态的销毁工作。
在基于Setup/Teardown的测试框架中,测试用例的一般过程包括四部分: Setup -> 执行用例,与被测软件进行交互 -> 验证结果,与期望值进行比较 -> Teardown,回到初始状态。
我们可以定义不同级别的Setup/Teardown方法,来实现不同范围的Fixture共享。例如,在用例中定义的Setup,只在这个用例执行之前调用;在Suite中定义的Setup,只在Suite执行之前调用,在Suite中的所有测试用例执行时不再调用。
基于Setup/Teardown的Fixture实现方式存在时间长,应用范围广,"江湖地位高"。不过,近年来,出现另外一种Fixture实现方式,渐渐被人认识。它就是Pytest Fixture。
Pytest是Python单元测试框架"三剑客"之一。与Python Unitest, Python Nose不同的是,Pytest不仅支持单元测试,还支持各种复杂的功能测试,UI测试和端到端测试。Pytest"宽应用光谱"的原因之一,是它采用了新的Fixture实现方式。
在Pytest中,基于@pytest.fixture装饰器,任意Python函数都可以被注册为Fixture。Fixture的名字即是Python函数的名字。测试用例(即测试函数)通过将Fixture名字声明在输入参数表中的方式,来选择和使用Fixture。
将Fixture定义为函数,可以实现Fixture的模块化。Fixture既可以调用各种库和模块,也可以被其他Fixture以输入参数声明的方式嵌套调用。与Setup/Teardown不同的是,Pytest Fixture不仅可以在Case和Suite范围共享,还可以在Package和Session范围共享。
以输入参数声明方式调用Fixture的另一个好处是可以实现细粒度的Fixture选择和组合。每个测试用例都可以将各自所依赖的Fixture放在输入参数表中,而将不依赖的Fixture剔除。重复出现的Fixture组合可以被封装成一个新的Fixture。Setup/Teardown方式就没有这种灵活度。
另外,Pytest Fixture支持参数化。针对同一个测试用例,根据不同的参数配置,可以在执行用例的过程中动态地调用不同的Fixture。这样,可以实现一个测试用例覆盖多种场景的需求。基于参数化的Fixture,数据驱动测试更容易实现,也更好维护。
大家可以看到,与传统的Setup/Teardown模式相比,Pytest Fixture模式使用更加方便,功能也更强大。个人认为,Pytest Fixture模式未来的前景值得期待。
总结一下,本文介绍了: 1) Test Fixture的产生背景和意义, 2) Test Fixture的概念和特点,3) 两种Test Fixture实现模式: Setup/Teardown和Pytest Fixture各自的含义和特点。
希望通过阅读本文,大家对"Test Fixture"不再陌生或迷糊,这方面的认识和理解能够上一个台阶。