什么是单元测试
单元测试是用来对最小可测试单元(如一个函数、一个类或者一个模块)进行检查和验证。尤其是在对代码进行修改或重构后,可以快速准确的定位出现问题的问题。
为什么要做单元测试
写代码的首要目标就是实现需求。在此基础上,就要尽可能提高代码的质量和可维护性。而单元测试,就是提高代码质量和可维护性的一种方式,也是成本最低的一种方式。因为尽早排除尽可能过的bug,可以减少在后续阶段解决bug的成本(包括时间成本、人力成本等)。
单元测试的基本原则
- 单元测试代码要非常简单,如果测试代码过于复杂,则测试代码本身也可能存在bug
- 通过单元测试,并不意味着所测代码没有bug
Python单元测试框架
- Unittest:Python标准库自带的单元测试工具,也称为PyUnit,是JUnit(Java语言的单元测试)的Python版本。
- Doctest:Python标准库自带的单元测试工具,适合简单的单元测试。其工作原理是在docstring中寻找测试用例并执行,比较输出结果与期望值是否符合。
- Pytest:Python的一种单元测试框架,与Unittest测试框架类似。其特点是拥有众多高级功能和插件。
- Nose:继承自Unittest,但比Unittest易用,而且能够自动识别测试用例。任何符合正则表达式“(?:^|[b_.-])[Tt]est”的类、函数、文件或目录,以及TestCase的子类都会被识别并执行。
- Unittest2:Unittest的升级版。
Unittest
Unittest是Python的标准测试库,无需额外安装,是目前Python使用较为官方的测试框架。下面通过实例,一步一步了解如何使用Unittest。
待测试代码如下,用于验证email和密码。
# -*- coding: utf-8 -*-
import re
class CheckUserInfo:
def check_mail(self, mail):
flag = False
str = '^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+){0,4}@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+){0,4}$'
if re.match(str, mail):
flag = True
return flag
def check_pwd_len(self, pwd):
'''密码长度不超过8位'''
return len(pwd)>=8
def check_pwd_contain_leter(self, pwd):
'''密码包含大小写英文字母'''
flag = False
pattern = re.compile('[A-Z][a-z]+')
match = pattern.findall(pwd)
if match:
flag = True
return flag
def check_pwd_contain_num(self, pwd):
'''密码包含数字'''
flag = False
pattern = re.compile('[0-9]+')
match = pattern.findall(pwd)
if match:
flag = True
return flag
TestCase — 单元测试用例
对上面代码中的每个方法进行测试,就需要实现一个继承unittest.TestCase 测试用例类。
# -*- coding: utf-8 -*-
import unittest
from com.unit_test.check_user_info import CheckUserInfo
class CheckUserInfoTestCase(unittest.TestCase):
def __init__(self, *args, **kwargs):
super(CheckUserInfoTestCase, self).__init__(*args, **kwargs)
self.check_user_info = CheckUserInfo()
@classmethod
def setUpClass(cls):
print('setUpClass\n\n')
@classmethod
def tearDownClass(cls):
print('tearDownClass')
def setUp(self):
print('setUp')
def tearDown(self):
print('tearDown\n')
def test_check_mail(self):
print('test_check_mail')
self.assertEqual(True, self.check_user_info.check_mail('aa@bb'))
self.assertEqual(False, self.check_user_info.check_mail('aa.bb'))
def test_check_pwd_len(self):
print('test_check_pwd_len')
self.assertEqual(True, self.check_user_info.check_pwd_len('12345678'))
self.assertEqual(False, self.check_user_info.check_pwd_len(''))
self.assertEqual(False, self.check_user_info.check_pwd_len('1'))
self.assertEqual(True, self.check_user_info.check_pwd_len('123456789'))
def test_check_pwd_contain_letter(self):
print('test_check_pwd_contain_leter')
self.assertEqual(True, self.check_user_info.check_pwd_contain_leter('1qazXSW@'))
self.assertEqual(False, self.check_user_info.check_pwd_contain_leter('12345678'))
self.assertEqual(False, self.check_user_info.check_pwd_contain_leter(''))
def test_check_pwd_contain_num(self):
print('test_check_pwd_contain_num')
self.assertEqual(True, self.check_user_info.check_pwd_contain_num('1qazXSW@'))
self.assertEqual(False, self.check_user_info.check_pwd_contain_num('qwertasdfg'))
self.assertEqual(False, self.check_user_info.check_pwd_contain_num(''))
def aaa(self):
print('test_check_pwd_contain_num')
self.assertEqual(True, self.check_user_info.check_pwd_contain_num('1qazXSW@'))
if __name__ == '__main__':
unittest.main()
执行结果如下:
...
setUpClass
setUp
test_check_mail
tearDown
setUp
test_check_pwd_contain_leter
tearDown
setUp
test_check_pwd_contain_num
tearDown
setUp
test_check_pwd_len
tearDown
tearDownClass
Ran 4 tests in 0.013s
OK
Process finished with exit code 0
使用“unittest.main()”,通过观察上面的测试代码以及执行结果,可以发现:
testXXX方法
使用“unittest.main()”执行测试用例时,每个测试方法必须以“test”开头,否则Unittes无法识别。例如,上面代码中的“aaa”方法未执行。
测试方法执行顺序
每个测试方法执行顺序并不是按照代码的书写顺序执行,而是按照前缀“test”后的字母顺序执行的。
setUp()方法
setUp()方法一般用来准备测试环境,在每个测试方法执行之前执行。
tearDown()方法
tearDown()方法一般用来清理测试环境,在每个测试方法执行之后执行,且不论测试方法执行是否成功。
setUpClass()方法
setUpClass()方法一般用来在所有测试方法执行之前准备一次测试环境,必须使用“@classmethod”装饰符,在所有测试方法执行前执行。
tearDownClass()
tearDownClass()方法一般用来在所有测试方法结束之后清理一次测试环境,必须使用“@classmethod”装饰符,在所有测试方法都执行完成否执行。
skip装饰器
不希望执行某个测试方法时,可以使用skip装饰器。skip装饰器共有三种:unittest.skip(reason)、unittest.skipIf(condition, reason)、unittest.skipUnless(condition, reason)。
- unittest.skip(reason):无条件跳过
# -*- coding: utf-8 -*-
import unittest
from com.unit_test.check_user_info import CheckUserInfo
class CheckUserInfoTestCase(unittest.TestCase):
......
@unittest.skip('skip test_bbb method')
def test_bbb(self):
print('test_bbb')
......
部分执行结果如下:
...
Skipped: skip test_bbb method
...
- unittest.skipIf(condition, reason):当condition为True时跳过
# -*- coding: utf-8 -*-
import unittest
from com.unit_test.check_user_info import CheckUserInfo
class CheckUserInfoTestCase(unittest.TestCase):
......
@unittest.skipIf(1<2, 'skip test_ccc method')
def test_ccc(self):
print('test_ccc')
......
部分执行结果如下:
...
Skipped: skip test_ccc method
...
- unittest.skipUnless(condition, reason):当condition为False时跳过
# -*- coding: utf-8 -*-
import unittest
from com.unit_test.check_user_info import CheckUserInfo
class CheckUserInfoTestCase(unittest.TestCase):
......
@unittest.skipUnless(1>2, 'skip test_ddd method')
def test_ddd(self):
print('test_ddd')
......
部分执行结果如下:
...
Skipped: skip test_ddd method
...
那么,如何控制测试方法的执行顺序呢?如何执行非“test”开头的测试方法?接下来了解一下TestSuite。
TestSuite — 单元测试用例合集
通过上面的测试用例代码,可以发现无法指定测试方法的顺序。那么通过unittest.TestSuite,就可以按照业务逻辑顺序执行测试方法。
# -*- coding: utf-8 -*-
import unittest
from com.unit_test.check_user_info_test_case import CheckUserInfoTestCase
if __name__ == '__main__':
suite = unittest.TestSuite()
tests = [
CheckUserInfoTestCase('test_check_mail'),
CheckUserInfoTestCase('test_check_pwd_len'),
CheckUserInfoTestCase('test_check_pwd_contain_letter'),
CheckUserInfoTestCase('test_check_pwd_contain_num')
]
suite.addTests(tests)
runner = unittest.TextTestRunner()
runner.run(suite)
执行结果如下:
...
Ran 4 tests in 0.002s
OK
setUpClass
setUp
test_check_mail
tearDown
setUp
test_check_pwd_len
tearDown
setUp
test_check_pwd_contain_leter
tearDown
setUp
test_check_pwd_contain_num
tearDown
tearDownClass
Process finished with exit code 0
通过数组指定测试方法的执行顺序
tests = [
CheckUserInfoTestCase('test_check_mail'),
CheckUserInfoTestCase('test_check_pwd_len'),
CheckUserInfoTestCase('test_check_pwd_contain_letter'),
CheckUserInfoTestCase('test_check_pwd_contain_num')
]
suite.addTests(tests)
通过addTests()方法指定测试方法的执行顺序
suite.addTest(CheckUserInfoTestCase('test_check_mail'))
suite.addTest(CheckUserInfoTestCase('test_check_pwd_len'))
suite.addTest(CheckUserInfoTestCase('test_check_pwd_contain_letter'))
suite.addTest(CheckUserInfoTestCase('test_check_pwd_contain_num'))
无法指定测试方法执行顺序的unittest.TestLoader()
通过unittest.TestLoader()加载TestCase,测试方法是按照前缀“test”后的字母顺序执行的。
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(CheckUserInfoTestCase))
执行非“test”开头的测试方法
在TestCase中的示例代码中,可以看到,测试方法名不是以“test”开头时是不会被执行的。那么,通过TestSuite就可以完美解决这个问题。
suite.addTest(CheckUserInfoTestCase('aaa'))