引言
本文主要介绍了如下内容
单元测试的定义;
python中如何写基础的unittest单元测试;
详解unittest中的基础知识点:断言、测试固件、suite、如何控制用例执行顺序、如何把测试结果输出到文件;
详解unittest中的高级知识点:@unittest.skip、@unittest.expectedFailure、failfast、参数化;
什么是单元测试
单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。
比如对于函数abs(),我们可以编写的测试用例为:
(1)输入正数,比如1、1.2、0.99,期待返回值与输入相同
(2)输入复数,比如-1、-1.2、-0.99,期待返回值与输入相反
(3)输入0,期待返回0
(4)输入非数值类型,比如None、[]、{}、期待抛出TypeError
把上面这些测试用例放到一个测试模块里,就是一个完整的单元测试。
unittest工作原理
unittest中最核心的四部分是:TestCase,TestSuite,TestRunner,TestFixture
(1)一个TestCase的实例就是一个测试用例。测试用例就是指一个完整的测试流程,包括测试前准备环境的搭建(setUp),执行测试代码(run),以及测试后环境的还原(tearDown)。元测试(unit test)的本质也就在这里,一个测试用例是一个完整的测试单元,通过运行这个测试单元,可以对某一个问题进行验证。
(2)而多个测试用例集合在一起,就是TestSuite,而且TestSuite也可以嵌套TestSuite。
(3)TestLoader是用来加载TestCase到TestSuite中的。
(4)TextTestRunner是来执行测试用例的,其中的run(test)会执行TestSuite/TestCase中的run(result)方法
(5)测试的结果会保存到TextTestResult实例中,包括运行了多少测试用例,成功了多少,失败了多少等信息。
综上,整个流程就是首先要写好TestCase,然后由TestLoader加载TestCase到TestSuite,然后由TextTestRunner来运行TestSuite,运行的结果保存在TextTestResult中,整个过程集成在unittest.main模块中。核心类图关系如下:
unittest的基本使用方法
1)import unittest
2)定义一个继承自unittest.TestCase的测试用例类
3)定义setUp和tearDown,在每个测试用例前后做一些辅助工作。
4)定义测试用例,名字以test开头
5)一个测试用例应该只测试一个方面,测试目的和测试内容应很明确。主要是调用assertEqual、assertRaises等断言方法判断程序执行结果和预期值是否相符。
6)调用unittest.main()启动测试
下面举个实例,来看看unittest如何测试一个简单的函数
测一个简单的加减乘除接口
mathfunc.py文件代码如下:
def add(a, b):
return a + b
def minus(a, b):
return a - b
def multi(a, b):
return a * b
def divide(a, b):
return a / b
test_mathfunc.py文件代码如下:
import unittest
from mathfunc import *
class TestMathFunc (unittest.TestCase):
def test_add(self):
self.assertEqual (3, add (1, 2))
def test_minus(self):
self.assertEqual (1, minus (3, 2))
def test_multi(self):
self.assertEqual (6, multi (3, 2))
def test_divide(self):
self.assertEqual (2, divide (6, 2))
if __name__ == '__main__':
unittest.main ()
在pycharm中运行该用例:
输出:
可以看到一共运行了4个测试,失败了1个,并且给出了失败原因,2 != 3.0,也就是说我们的divide方法是有问题的。上图的左侧显示了用例的运行状态,右侧显示了具体的用例运行信息。
备注:在unitest.main()中加verbosity参数可以控制输出的错误报告的详细程度,用命令行的方式运行脚本会发现区别。
0 (静默模式): 你只能获得总的测试用例数和总的结果 ;
1 (默认模式): 非常类似静默模式 只是在每个成功的用例前面有个“.” 每个失败的用例前面有个 “F” ;
2 (详细模式):测试结果会显示每个测试用例的所有相关的信息;
设置unitest.main(verbosity=2),在cmd中运行 python test_mathfunc.py,运行结果如下:
test_add (__main__.TestMathFunc) ... ok
test_divide (__main__.TestMathFunc) ... FAIL
test_minus (__main__.TestMathFunc) ... ok
test_multi (__main__.TestMathFunc) ... ok
======================================================================
FAIL: test_divide (__main__.TestMathFunc)
----------------------------------------------------------------------
Traceback (most recent call last):
File "unittest_demo.py", line 32, in test_divide
self.assertEqual (2, divide (6, 2))
AssertionError: 2 != 3.0
----------------------------------------------------------------------
Ran 4 tests in 0.003s
断言
在测试用例中,执行完测试用例后,最后一步是判断测试结果是pass还是fail,自动化测试脚本里面一般把这种生成测试结果的方法称为断言(assert)。
Python中的断言类型丰富,主要包括:
基础断言
exceptions, warnings, 日志信息断言
特殊断言
集合断言
组织TestSuite
上面的测试用例在执行的时候没有按照顺序执行,如果想要让用例按照你设置的顺序执行就用到了TestSuite。我们添加到TestSuite中的case是会按照添加的顺序执行的。现在我们只有一个测试文件,如果有多个测试文件,也可以用TestSuite组织起来。继续上面加减乘除的例子,现在再新建一个文件,test_suite.py,代码如下:
import unittest
from test_mathfunc import TestMathFunc
if __name__ == '__main__':
suite = unittest.TestSuite()
tests = [TestMathFunc("test_add"), TestMathFunc("test_minus"), TestMathFunc("test_divide")]
suite.addTests(tests)
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
执行结果如下:
test_add (test_mathfunc.TestMathFunc) ... ok
test_minus (test_mathfunc.TestMathFunc) ... ok
test_divide (test_mathfunc.TestMathFunc) ... FAIL
======================================================================
FAIL: test_divide (test_mathfunc.TestMathFunc)
----------------------------------------------------------------------
Traceback (most recent call last):
File "D:\pythonWorkspace\HTMLTest\test_mathfunc.py", line 20, in test_divide
self.assertEqual(2, divide(6, 2))
AssertionError: 2 != 3.0
----------------------------------------------------------------------
Ran 3 tests in 0.000s
FAILED (failures=1)
当测试用例达到上百条时,上述加载用例的方法效率就变得很差了,unittest提供了discover方法来解决这一问题,该方法可以自动识别测试用例,具体用法如下:
discover(start_dir,pattern='test*.py',top_level_dir= None)
找到指定目录下所有测试模块,并可递归查到子目录下的测试模块,只有匹配到文件名时才加载
start_dir:要测试的模块名或测试用例目录
pattern='case*.py':表示用例文件名的匹配原则。此处匹配以“case”开头的.py 类型的文件,* 表示任意多个字符
top_level_dir= None 测试模块的顶层目录,如果没有顶层目录,默认为None
实例代码:
test_dir='./' #表示当前目录
discover=unittest.defaultTestLoader.discover(test_dir,pattern='case*.py')
runner = unittest.TextTestRunner()
runner.run(discover)
suite相关方法
loadTestsFromTestCase(testCaseClass)
testCaseClass必须是TestCase的子类(或孙类也行)
loadTestsFromModule(module, pattern=None)
test case所在的module
loadTestsFromName(name, module=None)
name是一个string,string需要是是这种格式的“module.class.method”
loadTestsFromNames(name, module=None)
names是一个list,用法与上同
discover(start_dir, pattern=’test*.py’, top_level_dir=None)
从python文件中获取test cases
用例顺序执行
unittest在执行用例(test_xxx)时,并不是按从上到下的顺序执行,有特定的顺序。unittest框架默认根据ACSII码的顺序加载测试用例,数字与字母的顺序为:0~9,A~Z,a~z。
对于类来说,class TestAxx 会优先于class TestBxx被执行。
对于方法来说,test_aaa()方法会有优先于test_bbb()被执行。
所以我们设计用例时,如果各个用例有执行先后的顺序关系,可以参考如下:
def test_1_***
def test_2_***
…
def test_n_***
将结果输出到文件
现在我们的测试结果只能输出到控制台,现在我们想将结果输出到文件中以便后续可以查看。
将test_suite.py进行一点修改,代码如下:
import unittest
from test_mathfunc import TestMathFunc
if __name__ == '__main__':
suite = unittest.TestSuite()
tests = [TestMathFunc("test_add"), TestMathFunc("test_minus"), TestMathFunc("test_divide")]
suite.addTests(tests)
with open('UnittestTextReport.txt', 'a') as f:
runner = unittest.TextTestRunner(stream=f, verbosity=2)
runner.run(suite)
运行该文件,就会发现目录下生成了'UnittestTextReport.txt,所有的执行报告均输出到了此文件中。
测试固件——testfixture
当遇到要启动一个数据库这种情况时,只想在开始时连接上数据库,在结束时关闭连接。那么可以使用setUp和tearDown函数。
class TestDict(unittest.TestCase):
def setUp(self):
print ('setUp...')
def tearDown(self):
print ('tearDown...')
这两个方法在每个测试方法执行前以及执行后执行一次,setUp用来为测试准备环境,tearDown用来清理环境,以备后续的测试。
如果想要在所有case执行之前准备一次环境,并在所有case执行结束之后再清理环境,我们可以用setUpClass()与tearDownClass(),代码格式如下:
class TestMathFunc(unittest.TestCase):
@classmethod
def setUpClass(cls):
createConnection()
@classmethod
def tearDownClass(cls):
closeConnection()
同理,unittest也提供了模块的fixture
def setUpModule():
createConnection()
def tearDownModule():
closeConnection()
跳过某个case——@unittest.skip
unittest提供了几种方法可以跳过case
(1)skip装饰器
代码如下
import unittest
from mathfunc import *
class TestMathFunc(unittest.TestCase):
.....
@unittest.skip("i don't want to run this case.")
def test_minus(self):
self.assertEqual(1, minus(3, 2))
输出:
test_add (test_mathfunc.TestMathFunc) ... ok
test_minus (test_mathfunc.TestMathFunc) ... skipped "i don't want to run this case."
test_divide (test_mathfunc.TestMathFunc) ... FAIL
======================================================================
FAIL: test_divide (test_mathfunc.TestMathFunc)
----------------------------------------------------------------------
Traceback (most recent call last):
File "D:\pythonWorkspace\HTMLTest\test_mathfunc.py", line 28, in test_divide
self.assertEqual(2, divide(6, 2))
AssertionError: : 2 != 3.0
----------------------------------------------------------------------
Ran 3 tests in 0.000s
FAILED (failures=1, skipped=1)
skip装饰器一共有三个
unittest,skip(reason):无条件跳过
unittest.skipIf(condition, reason):当condition为True时跳过
unittest.skipUnless(condition, reason):当condition为False时跳过
(2)TestCase.skipTest()方法
class TestMathFunc(unittest.TestCase):
...
def test_minus(self):
self.skipTest('do not run this.')
self.assertEqual(1, minus(3, 2))
输出:
test_add (test_mathfunc.TestMathFunc) ... ok
test_minus (test_mathfunc.TestMathFunc) ... skipped 'do not run this.'
test_divide (test_mathfunc.TestMathFunc) ... FAIL
======================================================================
FAIL: test_divide (test_mathfunc.TestMathFunc)
----------------------------------------------------------------------
Traceback (most recent call last):
File "D:\pythonWorkspace\HTMLTest\test_mathfunc.py", line 20, in test_divide
self.assertEqual(2, divide(6, 2))
AssertionError: 2 != 3.0
----------------------------------------------------------------------
Ran 3 tests in 0.000s
FAILED (failures=1, skipped=1)
预期失败—— @unittest.expectedFailure
@unittest.expectedFailure标记用例为预期是失败的,如果用例执行失败,认为是success;如果用例执行成功,则认为是failure。
例如代码:
import unittest
class ExpectedFailureTestCase(unittest.TestCase):
@unittest.expectedFailure
def test_fail1(self):
self.assertEqual(1, 0,)
@unittest.expectedFailure
def test_fail2(self):
self.assertEqual (1, 1)
@unittest.expectedFailure
def test_fail3(self):
self.assertEqual (1, 1)
短路测试—— failfast
failfast是TestResult的一个属性,缺省为False。 如果failfast为True,一旦测试集中有测试案例failed或发生error立即终止当前整个测试执行,跳过剩下所有测试用例。
实现“短路测试”,设置failfast为True,具体操作如下:
unittest.main(failfast=True)
unittest.TextTestRunner(failfast=True)
实例如下:
import unittest
class TestMathFunc (unittest.TestCase):
def test_add(self):
self.assertEqual (3, add (1, 2))
def test_minus(self):
self.assertEqual (1, minus (3, 2))
def test_multi(self):
self.assertEqual (6, multi (3, 2))
def test_divide(self):
self.assertEqual (2, divide (6, 2))
if __name__ == '__main__':
unittest.main (failfast=True)
使用命令行的方式运行代码,结果如下:
test_add (__main__.TestMathFunc) ... ok
test_divide (__main__.TestMathFunc) ... FAIL
======================================================================
FAIL: test_divide (__main__.TestMathFunc)
----------------------------------------------------------------------
Traceback (most recent call last):
File "unittest_demo.py", line 32, in test_divide
self.assertEqual (2, divide (6, 2))
AssertionError: 2 != 3.0
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
可以看到用例test_divide失败后,用例test_minus和test_multi没有继续运行
用例的参数化
unittest从Python3.4版本开始支持参数化,使用方法subTest()
具体代码如下
class ParaTest(unittest.TestCase):
def test_1(self):
for i in range(0, 4):
with self.subTest(i=i):
self.assertEqual(i % 2, 0)
输出结果如下:
详细的错误信息如下: