使用unittest模块部署单元测试
By 青衣极客 In 2019-10-07
python使用者常常被一个问题困扰:线下运行没问题的代码到线上就崩溃,而且很多时候还是一些简单的错误。基本上写完一段代码之后都会简单测试一下,然后大规模运行起来,但是对于生产环境而言,这显然是十分草率的。python是一种动态语言,而不是c/c++这种静态编译语言。c/c++在编译的过程中能发现很多简单的错误,比如类型错误,语法问题等等,而python则只能在运行到该句代码时才会出错。但是线下简单的测试只是将代码整体跑一下,对程序而言覆盖面太小,难以发现一些分支上存在的错误。因此需要引入“单元测试”。其他语言也有各自单元测试的框架,比如gtest就是一款很不错的c++单元测试框架。只是对于python程序而言,单元测试显得更加重要,因为这是我们保证生产环境下系统可靠性的唯一方法。至于有些开发者所具有的迷之自信,不经测试就认为自己的代码没有问题,这一点在生产中是完全不能接受的。下面定义一组基本运算所用到的函数,然后讲述使用python内置模块unittest进行单元测试的方法。
class BasicOp:
"""
Class to package basic operations for computing
"""
@staticmethod
def add(a, b):
return a+b
@staticmethod
def minus(a, b):
return a-b
@staticmethod
def multiple(a, b):
return a*b
@staticmethod
def divide(a, b):
return a/b
1. 使用unittest的基本方法
unittest模块是以TestCase类来组织测试代码的,即测试代码封装在unittest.TestCase子类中名称以test_开头的函数中。在编写完成测试代码之后,直接调用unittest.main()即可。下面演示一下对BasicOp类中封装的四个基本函数的单元测试。
import unittest
class TestBasicOp(unittest.TestCase):
"""
Class to package all test case for class BasicOp
"""
def test_add(self):
self.assertEqual(2, BasicOp.add(1,1))
def test_minus(self):
self.assertEqual(2, BasicOp.minus(3,1))
def test_multiple(self):
self.assertEqual(2, BasicOp.multiple(1,2))
def test_divide(self):
self.assertEqual(1, BasicOp.divide(3,2))
unittest.main(argv=[''], verbosity=2, exit=False)
test_add (__main__.TestBasicOp) ... ok
test_divide (__main__.TestBasicOp) ... FAIL
test_minus (__main__.TestBasicOp) ... ok
test_multiple (__main__.TestBasicOp) ... ok
======================================================================
FAIL: test_divide (__main__.TestBasicOp)
----------------------------------------------------------------------
Traceback (most recent call last):
File "<ipython-input-4-c0c2e18fc6e2>", line 12, in test_divide
self.assertEqual(1, BasicOp.divide(3,2))
AssertionError: 1 != 1.5
----------------------------------------------------------------------
Ran 4 tests in 0.004s
FAILED (failures=1)
<unittest.main.TestProgram at 0x102a3d610>
从测试结果发现,一共有4个单元测试,其中3个测试通过,分别为:test_add, test_minus, test_multiple;还有一个test_divide测试失败。结果显示失败原因是函数返回值与预期数值存在差异。根据测试结果很容易找到代码中存在的问题并修复。这里演示的例子非常简单,所以覆盖面非常窄,在真正的项目中,需要严密设计单元测试的各种情形,以保证在一些边界条件下程序仍然能够正常执行。
2. 加载和清理测试环境
在部署测试时,有时需要进行一些环境的加载和清理,比如读取一些资源或者测试用例,或者删除一些变量以免对后续测试产生影响。unittest提供了两组函数来实现这种需求。第一组setUp函数在每个测试函数运行之前运行,tearDown函数在每个测试函数运行完成之后运行;第二组setUpClass函数在所有测试函数运行之前运行,tearDownClass函数在所有测试函数运行之后运行。即setUp和tearDown是与每一个测试函数相配套的,setUpClass和tearDownClass是与当前测试类中所有测试函数相匹配的。说起来可能太抽象,还是来看代码演示。
import unittest
class TestBasicOp(unittest.TestCase):
"""
Class to package all test case for class BasicOp
"""
def setUp(self):
print('init env for each block of testing code ...')
def tearDown(self):
print('clear env for each block of testing code ...')
@classmethod
def setUpClass(cls):
print('init env for all testing code ...')
@classmethod
def tearDownClass(cls):
print('clear env for all testing code ...')
def test_add(self):
self.assertEqual(2, BasicOp.add(1,1))
def test_minus(self):
self.assertEqual(2, BasicOp.minus(3,1))
def test_multiple(self):
self.assertEqual(2, BasicOp.multiple(1,2))
def test_divide(self):
self.assertEqual(1, BasicOp.divide(3,2))
unittest.main(argv=[''], verbosity=2, exit=False)
test_add (__main__.TestBasicOp) ... ok
test_divide (__main__.TestBasicOp) ... FAIL
test_minus (__main__.TestBasicOp) ... ok
test_multiple (__main__.TestBasicOp) ...
init env for all testing code ...
init env for each block of testing code ...
clear env for each block of testing code ...
init env for each block of testing code ...
clear env for each block of testing code ...
init env for each block of testing code ...
clear env for each block of testing code ...
init env for each block of testing code ...
clear env for each block of testing code ...
clear env for all testing code ...
ok
======================================================================
FAIL: test_divide (__main__.TestBasicOp)
----------------------------------------------------------------------
Traceback (most recent call last):
File "<ipython-input-5-f44a7d78a12b>", line 25, in test_divide
self.assertEqual(1, BasicOp.divide(3,2))
AssertionError: 1 != 1.5
----------------------------------------------------------------------
Ran 4 tests in 0.003s
FAILED (failures=1)
<unittest.main.TestProgram at 0x102b49610>
3. 跳过某个测试
有时编写的测试还存在一些问题,有时一些测试已经过时,但是还不便删除,这时就可以使用跳过机制来避免运行该单元测试。unittest提供两种跳过方式,一种是使用修饰器unittest.skip,另一种使用函数self.skipTest。两种操作都非常简单,不过修饰器更方便一些。从下面演示的测试结果来看跳过了两个测试用例,与设计预期相符合。
import unittest
class TestBasicOp(unittest.TestCase):
"""
Class to package all test case for class BasicOp
"""
@unittest.skip('[unittest.skip], just for testing')
def test_add(self):
self.assertEqual(2, BasicOp.add(1,1))
def test_minus(self):
self.skipTest('[self.skipTest], just for testing')
self.assertEqual(2, BasicOp.minus(3,1))
def test_multiple(self):
self.assertEqual(2, BasicOp.multiple(1,2))
def test_divide(self):
self.assertEqual(1, BasicOp.divide(3,2))
unittest.main(argv=[''], verbosity=2, exit=False)
test_add (__main__.TestBasicOp) ... skipped '[unittest.skip], just for testing'
test_divide (__main__.TestBasicOp) ... FAIL
test_minus (__main__.TestBasicOp) ... skipped '[self.skipTest], just for testing'
test_multiple (__main__.TestBasicOp) ... ok
======================================================================
FAIL: test_divide (__main__.TestBasicOp)
----------------------------------------------------------------------
Traceback (most recent call last):
File "<ipython-input-6-f5ef6b9c41a7>", line 16, in test_divide
self.assertEqual(1, BasicOp.divide(3,2))
AssertionError: 1 != 1.5
----------------------------------------------------------------------
Ran 4 tests in 0.004s
FAILED (failures=1, skipped=2)
<unittest.main.TestProgram at 0x102b63410>
4. 生成测试文件
如果有需求是将测试结果写入文件中作为一份报告,那么该如何实现呢?unittest已经提供了这种需求的实现接口。使用unittest.TextTestRunner模块就可以完成。网上还有其他博主演示生成html文件的,不过需要额外的模块支持。而该模块在python3下无法正常运行,因此这里就只演示一下将测试结果写入带普通的文本文件中。
import unittest
class TestBasicOp(unittest.TestCase):
"""
Class to package all test case for class BasicOp
"""
def test_add(self):
self.assertEqual(2, BasicOp.add(1,1))
def test_minus(self):
self.assertEqual(2, BasicOp.minus(3,1))
def test_multiple(self):
self.assertEqual(2, BasicOp.multiple(1,2))
def test_divide(self):
self.assertEqual(1, BasicOp.divide(3,2))
# 将测试报告写入文件中
suite = unittest.TestSuite()
suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestBasicOp))
with open('../../output/unittest_reporter.txt', 'w') as fid:
runner = unittest.TextTestRunner(stream=fid, verbosity=2)
runner.run(suite)
# 查看文件内容
%cat ../../output/unittest_reporter.txt
test_add (__main__.TestBasicOp) ... ok
test_divide (__main__.TestBasicOp) ... FAIL
test_minus (__main__.TestBasicOp) ... ok
test_multiple (__main__.TestBasicOp) ... ok
======================================================================
FAIL: test_divide (__main__.TestBasicOp)
----------------------------------------------------------------------
Traceback (most recent call last):
File "<ipython-input-6-27adda2e2cf0>", line 13, in test_divide
self.assertEqual(1, BasicOp.divide(3,2))
AssertionError: 1 != 1.5
----------------------------------------------------------------------
Ran 4 tests in 0.002s
FAILED (failures=1)
到此python中使用unittest模块部署单元测试的操作介绍完毕。为自己的代码增加单元测试无论对于自己还是对于接手的维护人员来说都是一件大好事,毕竟没人愿意再把代码逐行看一遍,而且肉眼查看很难发现一些分支中存在的漏洞。希望大家都能用好单元测试,做一个更专业的开发者。

COMMENT
博客评论区功能由Github Issue提供,提交Issue时请以本文标题为话题。
"BG27-使用unittest模块部署单元测试"