一种数据与逻辑分离的Python单元测试工具
一种数据与逻辑分离的Python单元测试工具
几个概念
TestCase
TestCase是一个完整的测试单元,最小的测试执行实体,就是我们常说的测试用例。
TestSuite
以某种特性将测试用例组合到一起的测试用例集合被称作TestSuite,中文可以叫做测试套件。TestSuite可以包含TestCase,也可以嵌套TestSuite。
TestFixtrue
TestFixtrue是测试主程序运行时所需要的一切东西,可能是数据,可能是系统环境,也有可能是某个具体实例化类,中文可以称作测试固件。TestFixtrue包括构建(setUp)和销毁(tearDown)两个部分。例如,某个测试用例需要访问数据库,那么可以在setUp中建立数据库连接并初始化相关数据,在tearDown中释放数据库连接并还原相关数据。此外,TestFixtrue既可以存在于测试用例层面,也可以存在于测试套件层面。
单元测试框架
结合以上概念,一个测试用例的执行过程包括:
- 测试用例固件构建
- 执行测试用例主程序
- 测试用例固件销毁
一个测试套件的执行过程包括:
- 测试套件固件构建
- 测试用例1固件构建
- 执行测试用例1主程序
- 测试用例1固件销毁
- 测试用例2固件构建
- ……
- 测试用例n固件销毁
- 测试套件固件销毁
常用的单元测试工具
Python下常用的单元测试工具有unittest,PyUnit,nose。这三种工具的实现原理和使用方式大致相同。下面以unittest为例,来观察这些工具的特点。
import unittest
class CaculateTestCase(unittest.TestCase):
def setUp(self):
self.a = 1
self.b = 2
print 'set up the fixture'
def tearDown(self):
self.a = None
self.b = None
print 'tear down the fixture'
def testAdd(self):
assert (self.a+self.b) == 3, 'add function error'
def testMinus(self):
assert (self.a-self.b) == -1, 'minus function error'
if __name__ == '__main__':
unittest.main()
在上面的代码中,CaculateTestCase是有关运算的TestCase,它继承自unittest.TestCase类。CaculateTestCase重写了setUp和tearDown方法,分别对应fixture的构建和销毁代码。
在加载测试任务时,unittest会根据目标文件寻找unittest.TestCase的子类,并寻找以“test”开头的方法,这些方法分别对应一个测试用例主程序。
因此,CaculateTestCase的具体执行流程是:
- 执行CaculateTestCase中的setUp方法,进行固件构建
- 依次执行CaculateTestCase中以“test”开头的方法,即testAdd和testMinus方法
- 执行CaculateTestCase中的tearDown方法,进行固件销毁
分离数据与逻辑
在CaculateTestCase实例中,a和b变量是写死在程序中,脚本显得相当死板,没有什么可重用性。若让数据在以单一的文件存在,CaculateTestCase中只包含逻辑程序,测试工具在执行用例时,会根据数据文件初始化实例变量。如此一来,分离了数据和逻辑,逻辑部分变得可重用,可定制。在经过这般处理后,一个测试用例划分为测试用例逻辑程序和测试用例数据两个部分。在这里,我将测试用例逻辑程序抽象为TestDriver,即测试驱动器,而测试用例数据称作TestParam,即测试驱动数据。
TestDriver
与unittest等单元测试工具不同的是,我并没有将测试固件和主程序代码放在一个文件中或类中,而是用三个文件分别存放————setUp.py,tearDown.py,exec.py,并通过传递实例变量来共享TestCase实例。
因此,TestDriver的示例代码如下:
setUp.py代码:
def setUp(tc):
#固件构建代码
Pass
tearDown.py代码:
def tearDown (ts):
#固件构建代码
Pass
exec.py 代码:
def tc_case1 (tc):
#case1的测试逻辑代码
pass
def tc_case2(tc):
#case2的测试逻辑代码
pass
#……
def tc_caseN(tc):
#caseN的测试逻辑代码
pass
此外,TestDriver还包括默认的驱动参数信息,param.json,示例如下:
{
"key1":"value1",
"key2":"value2",
"key3":"value3"
}
TestCase
抽象出TestDriver后,在定义一个TestCase时不需要再写测试程序,而是指定一个TestDriver和TestParam,TestCase的示例文件如下:
{
"TestCase":"caseName",
"TestDriver": "dirverName",
"TestParam": {
"key1": "value1",
"key2": "value2",
"key3": "value3"
}
}
TestPlan
为了更好地组织TestCase故提出TestPlan这一概念,在执行完TestPlan后,会生成测试报告,发送测试通知。示例如下:
{
"TestPlan":"planName",
"CaseList":"tc_case1,tc_case2,tc_case3",
"EmailList":"xx@xx,xxx@xx",
"Crontab":"xxx"
}
CaseList即TestPlan所包含的TestCase,EmailList即TestPlan执行完毕后接收通知的邮箱信息,Crontab即TestPlan的定时器执行的crontab配置。
TestLib
当测试逻辑比较复杂时,TestDriver中代码可能也随之变得复杂。这时需要提供一完善的Python测试库,所有的业务操作均在测试库中,TestDriver只需要引用测试库进行业务操作,不包含业务逻辑的具体实现程序。随着TestLib的不断丰富,TestDriver中内容越来简明清晰,构建一个TestDriver也变得简单。于此,测试工作变得简单和更容易维护。
总结
“driver-case-plan”这种测试概念模型是对传统的单元测试框架的一种泛化抽象,它使得case在一定程度上更将通用,不受测试类型和业务类型的限制。此外,由于TestDriver的setUp、tearDown和exec是三个独立的文件,而不是像传统的单元测试工具这三部分是融合在一个文件中,在Web化时很难用一个单一的字段存储各部分的代码。再加上,TestCase和TestPlan也是JSON格式的文件,这是不是意味着该工具更容易Web化?在添加TestDriver后,TestCase和TestPlan的相关操作(添加用例计划、执行用例计划等)只需要在Web端处理即可?是不是可以依据该工具很容易地实现一个通用的CASE管理平台?