1. 认识unittest
什么是单元测试?单元测试负责对最小的软件设计单元(模块)进行验证,它使用软件设计文档中对模块的描述作为指南,对重要的程序分支进行测试以发现模块中的错误。在python语言下有诸多单元测试框架,如doctest、unittest、pytest、nose等,unittest框架(原名PyUnit框架)为Python语言自带的单元测试框架,Python 2.1及其以后的版本已将unittest作为一个标准模块放入Python开发包中,Python 3版本也一样。
1.1 认识单元测试
可能有人会问不用单元测试框架能写单元测试吗?答案是肯定的,单元测试本身就是通过一段代码去验证另一段代码,所以不用单元测试框架也可以写单元测试,下面就通过例子演示不用测试框架的单元测试。
首先创建一个被测试类:
#计算器类 class Count: def __init__(self,a,b): self.a = int(a) self.b = int(b) #计算加法 def add(self): return self.a +self.b
程序非常简单,创建一个Count类用于两个整数的计算,通过__init__()方法对两个数进行初始化,接着创建add()方法返回两个数相加的结果。
根据上面所实现的功能,不用测试框架所编写的单元测试如test1.py。
from test.test2 import Count #测试两个整数相加 class TestCount: def test_add(self): try: j = Count(2,3) add = j.add() assert(add == 5),'Integer addition result error!' except AssertionError as msg: print(msg) else: print('Test pass') #执行测试类的测试方法 mytest=TestCount() mytest.test_add()
首先,引入test2文件中的Count类,然后在test_add()方法中调用Count类并传入两个参数2和3,最后调用Count类中的add()方法对两个参数做加法运算,并通过assert()方法判断add()的返回值是否等于5。如果不相等则抛出自定义的“Integer addition result error!”异常信息,如果相等则打印“Test pass”。
运行结果1:
运行结果2:
不难发现这种测试方法存在的问题很多。首先,测试程序的写法没有一定的规范可以遵循,十个程序员完全可能写出十种不同的测试程序来,不统一的代码维护起来会十分麻烦。其次,需要编写大量的辅助代码才能进行单元测试,在test1.py中用于测试的代码甚至比被测试的代码还要多,而且这仅仅是一个测试用例,对一个单元模块来说,只编写一条测试用例显然是不够的。
为了让单元测试代码更容易维护和编写,最好的方式是遵循一定的规范来编写测试用例,这也是单元测试框架诞生的初衷。接下来讲如何通过unittest单元测试框架编写单元测试用例。
from test.test2 import Count import unittest class TestCount(unittest.TestCase): def setUp(self): print("test start") def test_add(self): j = Count(2, 3) self.assertEqual(j.add(),5) def tearDown(self): print("test end") if __name__ == '__main__': unittest.main()
分析上面的代码,首先引入unittest模块,创建TestCount类继承unittest的TestCase类,我们可以将TestCase类看成是特定类进行测试的集合。
setUp()方法用于测试用例执行前的初始化工作,这里只简单打印“test start”信息。tearDown()方法与setUp()方法相呼应,用于测试用例执行之后的善后工作,这里打印“test end”信息。
在test_add()中首先调用Count类并传入要计算的数,通过add()方法得到两数相加的返回值。这里不再使用繁琐的异常处理,而是调用unittest框架所提供assertEqual()方法对add()的返回值进行断言,判断两者是否相等,assertEqual()方法由TestCase类继承而来。
unittest提供了全局的main()方法,使用它可以方便的将一个单元测试模块变成可以直接运行的测试脚本。main()方法使用TestLoader类来搜索所有包含在该模块中以“test”命名开头的测试方法,并自动执行它们。
if __name__ == "__main__":语句说明
在后面的实例中我们会经常使用这个语句,在解释它之前先补充点python知识:
python文件的后缀为.py
.py文件既可以用来直接执行,就像一个小程序一样,也可以用来作为模块被导入
在python中导入模块一般使用的是import。
如果对这个不了解的,可以去找我写的python基础。
顾名思义,if就是如果的意思,在句子开始处加上if,就说明这个句子是一个条件语句。接着是__name__, __name__作为模块的内置属性,简单的说,就是.py文件的调用方式。最后是__mian__,如上所述,.py文件有两种使用方式:作为模块被调用和直接使用,如果它等于“__mian__”就表示是直接使用。
1.2 重要的概念
在unittest的文档中有4个重要的概念:test fixture、test case、test suite和test runner,只有理解了这几个概念才能理解单元测试的基本特征。
1.2.1 Test Case
一个TestCase的实例就是一个测试用例。什么是测试用例呢?就是一个完整的测试流程,包括测试前准备环境的搭建(setUp) 、实现测试过程的代码(run),以及测试后环境的还原(tearDown)。单元测试(unit test)的本质也就在这里,一个测试用例就是一个完整的测试单元,通过运行这个测试单元,可以对某一个功能进行验证。
1.2.2 Test Suite
一个概念的验证往往需要多个测试用例,可以把多个测试用例集合在一起来执行,这就产生了测试套件TestSuite的概念。Test Suite用来组装单个测试用例。可以通过addTest加载TestCase到TestSuite中,从而返回一个TestSuite实例。
1.2.3 Test Runner
测试的执行也是单元测试中非常重要的一个概念,一般单元测试框架中都会提供丰富的执行策略和执行结果。在unittest单元测试框架中,通过TextTestRunner类提供的run()方法来执行test suite/test case。test runner可以使用图形界面、文本界面,或返回一个特殊的值等方式来表示测试执行的结果。
1.2.4 Test Fixture
对一个测试用例环境的搭建和销毁,就是一个fixture,通过覆盖TestCase的setUp()和tearDown()方法来实现。有什么用呢?比如说在这个测试用例中需要访问数据库,那么可以在setUp()通过建立数据库连接来进行初始化,在tearDown()中清除数据库产生的数据,然后关闭连接等。
注意:tearDown的过程很重要,要为下一个test case留下一个干净的环境。
理解这几个概念后,我们再结合例子来学习下吧。
# from test.test2 import Count import unittest #计算器类 class Count: def __init__(self,a,b): self.a = int(a) self.b = int(b) #计算加法 def add(self): return self.a +self.b class TestCount(unittest.TestCase): def setUp(self): print("test start") def test_add(self): j = Count(2, 3) print("1执行了") self.assertEqual(j.add(),5) def test_add2(self): j = Count(5, 3) print("2执行了") self.assertEqual(j.add(),8) def test_add3(self): j = Count(4, 5) print("3执行了") self.assertEqual(j.add(), 9) def tearDown(self): print("test end") if __name__ == '__main__': # 构造测试集 suite = unittest.TestSuite() suite.addTest(TestCount("test_add2")) # 执行测试 runner = unittest.TextTestRunner() runner.run(suite)
在前面的例子的基础上编写了第二个测试用例test_add2()。由于第一条测试用例已经运行通过,因此这次只需运行第二条测试用例。在代码的最后,我们去掉了main()方法,采用构造测试集的方法来加载与运行测试用例,实现了有选择的执行测试用例。(这个时候我发现了一个bug,就是pycharm会执行全部测试用例,所以我把计算机类放到了test.py中,使用cmd来运行,就没有问题了)当然,也可以通过注释的方式注释掉第一条用例,但这种做法会导致页面太多注释,影响观看。。
首先,调用unittest框架的TestSuite()类来创建测试套件,通过它所提供的addTest()方法来添加测试用例test_add2()。接着调用unittest框架的TextTestRunner()类,通过它下面的run()方法来运行suite所组装的测试用例。
运行结果:
从运行结果可以看到,setUp/tearDown作用于测试用例的开始和结束。
1.3 断言方法
在执行用例的过程中,最终用例是否执行通过,是通过判断测试得到的实例结果与预期结果是否相等决定的。unittest框架的TestCase类提供了下面这些方法用于测试结果的判断。
assertEqual(first,second,msg=None):断言第一个参数和第二个参数是否相等,如果不相等则测试失败。msg为可选惨死你,用于定义测试失败时打印的信息。
import unittest class Test(unittest.TestCase): def setUp(self): print("test start") number = input("Enter a number:") self.number = int(number) def test_case(self): self.assertEqual(self.number,10,msg="Your input is not 10!") def tearDown(self): print("test end") 2 if __name__ == '__main__': unittest.main()
在setUp()方法中要求用户输入一个数,在test_case()中通过assertEqual()比较输入的数是否等于10,如果不相等则输出msg中定义的提示信息。
运行结果:
从运行结果看到,输入一个20,显然与预期的10不相等,msg所定义的提示信息告诉我们“Your input is not 10!”。
assertNotEqual(first,second,msg=None):与assertEqual相反,它用于断言第一个参数与第二个参数是否不相等,如果相等则测试失败。
assertTrue(expr,msg=None)和assertFalse(expr,msg=None):测试表达式是true或false。
下面来测试判断一个数是否为质数的功能,所谓的质数(又叫素数)是指只能被1和它本身整除的数。
def is_prime(n): if n <= 1: return False for i in range(2,n): if n % i == 0: return False return True
创建is_prinme()函数用于实现对质数的判断。当得到一个数字n后,首先判断它是否小于或等于1,如果小于或等于1,则直接返回False;如果大于1,则对其进行循环判断;若能整除2到其自身之间的任意一个数,则不为质数,返回False,否则返回True。
# from test.test2 import is_prime import unittest def is_prime(n): if n <= 1: return False for i in range(2,n): if n % i == 0: return False return True class Test(unittest.TestCase): def setUp(self): print("test start") def test_case(self): self.assertTrue(is_prime(8),msg="Is not prime!") def tearDown(self): print("test end") if __name__ == '__main__': unittest.main()
运行结果:
在调用is_prime()函数时分别传不同的值来执行测试用例,在上面的例子中传值为8,显然不是一个质数,所以通过assertTrue的断言得到的结果为False。
assertIn(first,second,msg=None)和assertNotIn(first,second,msg=None):断言第一个参数是否在第二个参数中,反过来讲,第二个参数是否包含第一个参数。
import unittest class Test(unittest.TestCase): def setUp(self): print("test start") def test_case(self): a = "hello" b = "hello world" self.assertIn(a,b,msg="a is not in b") def tearDown(self): print("test end") if __name__ == '__main__': unittest.main()
这个很好理解,定义字符串a为“hello”、b为“hello world”。通过assertIn判断b是否包含a,如果不包含则打印msg定义的信息。
assertIs(first,second,msg=None)和assertIsNot(first,second,msg=None):断言第一个参数和第二个参数是否为同一对象。
assertIsNone(expr,msg=None)和assertIsNotNone(expr,msg=None):断言表达式是否为None对象。
assertIsInstance(obj,cls,msg=None)和assertNotIsInstance(obj,cls,msg=None):断言obj是否为cls的一个实例。
断言obj是否为cls的一个实例。
在unittest中还提供了其他检查比较的方法,因为不常用,所以不再一一介绍。大家可以参考Python官方文档unittest章节进行学习。
1.4 组织单元测试用例
当我们增加被测功能和相应的测试用例之后,再来看看unittest单元测试框架是如何扩展和组织新增的测试用例的。
我们同样以前面的计算器为例,为其扩展sub()方法,用来计算两个数相减的结果。
#计算器类 class Count(): def __init__(self,a,b): self.a = int(a) self.b = int(b) #计算加法 def add(self): return self.a + self.b #计算剪发 def sub(self): return self.a - self.b
因为对计算器(calculator)又新增了减法功能(sub),所以需要针对新功能编写测试用例。
from test.test2 import Count import unittest class TestAdd(unittest.TestCase): def setUp(self): print("test add start") def test_add1(self): j = Count(2,3) print("add1") self.assertEqual(j.add(),5) def test_add2(self): j = Count(41,76) print("add2") self.assertEqual(j.add(),117) def tearDown(self): print("test add end") class TestSub(unittest.TestCase): def setUp(self): print("test sub start") def test_sub1(self): j = Count(2,3) print("sub1") self.assertEqual(j.sub(),-1) def test_sub2(self): j = Count(71,46) print("sub2") self.assertEqual(j.sub(),25) def tearDown(self): print("test sub end") if __name__ == '__main__': #构造测试集 suite = unittest.TestSuite() suite.addTest(TestAdd("test_add1")) suite.addTest(TestAdd("test_add2")) suite.addTest(TestAdd("test_sub1")) suite.addTest(TestAdd("test_sub2")) #运行测试集合 runner = unittest.TextTestRunner() runner.run(suite)
上例中创建了TestAdd()和TestSub()两个测试类,分别测试计算器中的add()和sub()两个功能。通过TestSuite类的addTest()方法把不同测试类中的测试方法组装到测试套装中。
运行结果:
通过测试结果可以看到,setUp()和tearDown()方法分别作用于每个测试用例的开始于结束。如果每个类中的setUp()和tearDown()所做的事情是一样的,那是不是可以封装一个自己的测试类呢?
from test.test2 import Count import unittest class MyTest(unittest.TestCase): def setUp(self): print("test add start") def tearDown(self): print("test add end") class TestAdd(unittest.TestCase): def test_add1(self): j = Count(2,3) print("add1") self.assertEqual(j.add(),5) def test_add2(self): j = Count(41,76) print("add2") self.assertEqual(j.add(),117) class TestSub(unittest.TestCase): def test_sub1(self): j = Count(2,3) print("sub1") self.assertEqual(j.sub(),-1) def test_sub2(self): j = Count(71,46) print("sub2") self.assertEqual(j.sub(),25) if __name__ == '__main__': #构造测试集 suite = unittest.TestSuite() suite.addTest(TestAdd("test_add1")) suite.addTest(TestAdd("test_add2")) suite.addTest(TestAdd("test_sub1")) suite.addTest(TestAdd("test_sub2")) #运行测试集合 runner = unittest.TextTestRunner() runner.run(suite)
创建MyTest()类的好处显而易见,对于测试类和测试方法来说,应将注意力放在具体的用例的编写上,无须关心setUp()和tearDown()所做的事情、不过前提条件是setUp()和tearDown()所做的事情是每个用例都需要的。
1.5 discover更多测试用例
随着软件功能的不断增加,对应的测试用例也会呈指数级增长。一个实现几十个功能的项目,对应的单元测试用例可能达到上百个。如果把所有的测试用例都写在一个test.py文件中,那么这个文件会越来越臃肿,后期维护起来也比较麻烦。需要将这些用例按照测试的功能进行拆分,分数到不同的测试文件中。
对上例中test.py文件的测试用例进行拆分,拆分后的目录结构如下:
testpro/
count.py
testadd.py
testsub.py
runtest.py
文件拆分后的实现代码:
testadd.py:
from test1.calculator import Count import unittest class TestAdd(unittest.TestCase): def setUp(self): print("test add start") def tearDown(self): print("test add end") def test_add1(self): j = Count(2,3) print("add1") self.assertEqual(j.add(),5) def test_add2(self): j = Count(41,76) print("add2") self.assertEqual(j.add(),117) if __name__ == '__main__': unittest.main()
testsub.py:
from test1.calculator import Count import unittest class TestSub(unittest.TestCase): def setUp(self): print("test add start") def tearDown(self): print("test add end") def test_sub1(self): j = Count(2,3) print("sub1") self.assertEqual(j.sub(),-1) def test_sub2(self): j = Count(71,46) print("sub2") self.assertEqual(j.sub(),25) if __name__ == '__main__': unittest.main()
接着创建用于执行所有用例的runtest.py文件。
runtest.py:
import unittest #加载测试文件 from test1.testadd import TestAdd from test1.testsub import TestSub #构造测试集 suite = unittest.TestSuite() suite.addTest(TestAdd("test_add1")) suite.addTest(TestAdd("test_add2")) suite.addTest(TestSub("test_sub1")) suite.addTest(TestSub("test_sub2")) if __name__ == '__main__': #执行测试 runner = unittest.TextTestRunner() runner.run(suite)
这样的拆分带来了好处,可以根据不同的功能创建不同的测试文件,甚至是不同的测试目录,测试文件中还可以将不同的小功能划分为不同的测试类,在类下编写测试用例,整体结构更加清晰。
这样的设计看上去很完美,但依然没有解决添加用例的问题,当用例达到成百上千条时,在runtest.py文件中通过addTest()添加/删除测试用例就变得非常麻烦,那么有没有方法让unittest单元测试框架自动识别测试用例呢?答案是肯定的,TestLoader类中提供的discover()方法可以解决这个问题。
TestLoader:该类负责根据各种标准加载测试用例,并将它们返回给测试套件。正常情况下,不需要创建这个类的实例。unittest提供了可以共享的defaultTestLoader类,可以使用其子类和方法创建实例,discover()方法就是其中之一。
discover(start_dir,pattern='test*.py',top_level_dir=None):找到指定目录下所有测试模块,并可递归查到子目录下的测试模块,只有匹配到文件名才能被加载。如果启动的不是顶层目录,那么顶层目录必须单独指定。
start_dir:要测试的模块名或测试用例目录
pattern='test*.py':表示用例文件名的匹配原则。此处匹配文件名以“test”开头的“.py”类型的文件,星号“*”表示任意多个字符
top_level_dir=None:测试模块的顶层目录,如果没有顶层目录,默认为None
现在通过discover()方法重新实现runtest.py文件的功能。
runtest.py:
import unittest #定义测试用例的目录为当前目录 test_dir = './' discover = unittest.defaultTestLoader.discover(test_dir,pattern='test*.py') if __name__ == '__main__': #执行测试 runner = unittest.TextTestRunner() runner.run(discover)
discover()方法会自动根据测试目录(test_dir)匹配查找测试用例文件(test*.py),并将查找到的测试用例组装到测试套件中,因此,可以直接通过run()方法执行discover,大大简化了测试用例的查找与执行。