不写测试用例的程序员,不是好测试


以前在 Java 中 junit 是必用的单元测试框架,而 Python 中的 junit 就是 unittest

第一个 TestCase

少废话,先看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: wxnacy([email protected])

import unittest

class Test(unittest.TestCase):

def test_hello_world(self):
self.assertEqual(11, len('Hello World'))

if __name__ == "__main__":
unittest.main()

1
2
3
4
5
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

我们实现了一个最简单的测试用例,unittest 中最基本的结构就是 TestCase,新建一个类继承 unittest.TestCase,类中每个 test 开头的方法都是一个测试函数(一定要 test 开头)

一个测试通过的用例,运行结果除了分割线,共三行

  • 第一行,每个测试函数的运行状态,共四种状态,. 为成功
  • 第二行,运行的测试条数及用时
  • 第三行,测试最终结果,一条失败则为 FAILED,并显示各种状态的条数

函数运行状态

测试函数的运行状态分四种

  • . 成功
  • F 失败
  • E 出错
  • s 跳过
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: wxnacy([email protected])
# Description:

import unittest

class Test(unittest.TestCase):

def add(self):
return 1 + 1

def divide(self):
return 0 / 0

def test_1(self):
'''test_1 func'''
self.assertEqual(2, self.add())

def test_2(self):
'''test_2 func'''
self.assertEqual(3, self.add())

def test_3(self):
'''test_3 func'''
self.assertEqual(3, self.divide())

@unittest.skip("skip this test case")
def test_4(self):
'''test_4 func'''
self.assertEqual(3, self.divide())

if __name__ == "__main__":
unittest.main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
.FEs
======================================================================
ERROR: test_3 (__main__.Test)
test_3 func
----------------------------------------------------------------------
Traceback (most recent call last):
File "simple.py", line 26, in test_3
self.assertEqual(3, self.divide())
File "simple.py", line 14, in divide
return 0 / 0
ZeroDivisionError: division by zero

======================================================================
FAIL: test_2 (__main__.Test)
test_2 func
----------------------------------------------------------------------
Traceback (most recent call last):
File "simple.py", line 22, in test_2
self.assertEqual(3, self.add())
AssertionError: 3 != 2

----------------------------------------------------------------------
Ran 4 tests in 0.001s

FAILED (failures=1, errors=1, skipped=1)

这是一个包含四种状态的例子,test_3(), test_2() 分别报错和测试失败,在结果中给出了出错的位置和具体原因

现在的测试程序运行结果是简要信息,这取决于 unittest.main() 方法中的 verbosity 参数,默认值为 1,0 则不输出每条结果,2 则为详细信息,如下:

1
2
3
4
5
6
7
8
9
test_1 (__main__.Test)
test_1 func ... ok
test_2 (__main__.Test)
test_2 func ... FAIL
test_3 (__main__.Test)
test_3 func ... ERROR
test_4 (__main__.Test)
test_4 func ... skipped 'skip this test case'
...

跳过某条测试

在上面的例子中,test_4() 方法是跳过的,unittest 中共有三种装饰器可以跳过程序

  • `@unittest.skip(reason)` 无条件跳过
  • `@unittest.skipIf(condition, reason)` 当条件为真时跳过
  • `@unittest.skipUnless(condition, reason)` 当条件为假时跳过
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    # Author: wxnacy([email protected])
    # Description:

    import unittest

    class Test(unittest.TestCase):

    def add(self):
    return 1 + 1

    @unittest.skip("skip test_1 case")
    def test_1(self):
    '''test_1'''
    self.assertEqual(2, self.add())

    def test_2(self):
    '''test_2'''
    self.skipTest('skip test_2 case')
    self.assertEqual(3, self.add())

    @unittest.skipIf(1 == 1, 'skip test_3 case')
    def test_3(self):
    self.assertEqual(3, self.add())

    @unittest.skipUnless(1 != 1, 'skip test_4 case')
    def test_4(self):
    self.assertEqual(3, self.add())

    if __name__ == "__main__":
    unittest.main(verbosity=2)
1
2
3
4
5
6
7
8
9
10
11
test_1 (__main__.Test)
test_1 ... skipped 'skip test_1 case'
test_2 (__main__.Test)
test_2 ... skipped 'skip test_2 case'
test_3 (__main__.Test) ... skipped 'skip test_3 case'
test_4 (__main__.Test) ... skipped 'skip test_4 case'

----------------------------------------------------------------------
Ran 4 tests in 0.000s

OK (skipped=4)

在详细结果中,会将跳过原因打印出来,并且在测试程序中,使用 self.skipTest(resaon) 也可以跳过程序

环境的准备和还原

一个完整的测试用例还需要一个环节,准备环境和还原环境,使用 setUp(), tearDown() 两个方法来完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: wxnacy([email protected])

import unittest

class Test(unittest.TestCase):

def add(self):
return 1 + 1

def setUp(self):
print('begin setUp')

def tearDown(self):
print('begin tearDown')

def test_1(self):
self.assertEqual(2, self.add())
print('run test_1')

def test_2(self):
self.assertEqual(2, self.add())
print('run test_2')

if __name__ == "__main__":
unittest.main()

1
2
3
4
5
6
7
8
9
10
11
begin setUp
run test_1
begin tearDown
.begin setUp
run test_2
begin tearDown
.
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

从打印结果中可以看出,每次执行一个测试方法前后都会执行准备和还原操作,如果你想要一个 TestCase 只执行一次准备和还原操作,则需要使用这两个方法

1
2
3
4
5
6
7
@classmethod
def setUpClass(cls):
print('begin setUpClass')

@classmethod
def tearDownClass(cls):
print('begin tearDownClass')

至此,我们已经经历一个完整的 TestCase 是什么样的,它有环境准备工作(setUp),运行测试代码(run),还原环境(tearDown),根据条件跳过(skip),共同组成了一个元测试(unit test),这样就是完整的测试用例

同时,我们需要开始考虑两个问题

  • 怎样控制测试程序的执行顺序
  • 如果同时执行多个测试用例

使用 TestSuite

针对刚才提出的问题,我们需要引入 TestSuite 的概念,多个测试用例组合起来,就是 TestSuite,TestSuite 也可以嵌套 TestSuite

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: wxnacy([email protected])

import unittest

class Test(unittest.TestCase):

def add(self):
return 1 + 1

def test_1(self):
self.assertEqual(2, self.add())

def test_2(self):
self.assertEqual(2, self.add())

def test_3(self):
self.assertEqual(2, self.add())


if __name__ == "__main__":
suite = unittest.TestSuite()

tests = [Test("test_1"), Test("test_3"), Test("test_2")]
suite.addTests(tests)

runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)

1
2
3
4
5
6
7
8
test_1 (__main__.Test) ... ok
test_3 (__main__.Test) ... ok
test_2 (__main__.Test) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

在将多个 TestCase 调价到 TestSuite 中后,我们用到了 unittest.TextTestRunner() 方法,他会执行 TestSuite 或 TestCase 测 run() 方法,并把结果输出到 TextTestResult 中,后续我们将怎样将结果在打印到文件中

然后如你所见,我们已经可以控制测试程序的执行顺序,同样的,通过这样的方式,我们还可以进行多个 TestCase 同时测试

输出到文件

每次测试结果都输出控制台,从可读性和持久化上都是个问题,我们需要想办法将结果打印到文件中,这里我们需要借助下第三方文件 HTMLTestRunner ,但是原版只支持 python2,我做了下改动,适配了 python3,地址为:https://github.com/wxnacy/study/blob/master/python/unittest_demo/HTMLTestRunner.py

使用方法请见修改 HTMLTestRunner.py 可以在 Python3 运行

检测方法

我们在刚才的例子中只用到了 assertEqual() 方法来检测结果是否相同,还有很多方法可以用来检测测试结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
assertEqual(a, b)	    # a == b
assertNotEqual(a, b) # a != b
assertTrue(x) # bool(x) is True
assertFalse(x) # bool(x) is False
# 3.1 新加
assertIs(a, b) # a is b
assertIsNot(a, b) # a is not b
assertIsNone(x) # x is None
assertIsNotNone(x) # x is not None
assertIn(a, b) # a in b
assertNotIn(a, b) # a not in b
# 3.2 新加
assertIsInstance(a, b) # isinstance(a, b)
assertNotIsInstance(a, b) # not isinstance(a, b)

更多方法见文档

03-17 01:25