python unittest 学习整理
unittest 官方文档学习
概念:
Test fixture
test fixture 表示为了开展一项或多项测试所需要进行的准备工作,以及所有相关的清理操作。举个例子,你想要测试一个网站的页面,每个测试用例执行前需要先打开网站首页,测试结束后需要关闭浏览器。
Test Case
一个测试用例是一个独立的测试单元。它检查输入特定的数据时的响应。 unittest
提供一个基类: TestCase
,用于新建测试用例。
Test Suite
test suite 是一系列的测试用例,或测试套件,或两者皆有。test suite 可以嵌套 test suite,它可以将很多测试用例打包,然后一起执行。
Test Runner
test runner 是一个用于执行和输出测试结果的组件。这个运行器可能使用图形接口、文本接口,或返回一个特定的值表示运行测试的结果。
基本示例
test.py
import unittest
class TestStringMethods(unittest.TestCase): # 测试类要继承 unittest.TestCase
def test_upper(self): # 以 test 开头的方法,表示一个测试
self.assertEqual('foo'.upper(), 'FOO') # 调用断言来判断测试
def test_isupper(self):
self.assertTrue('FOO'.isupper())
self.assertFalse('Foo'.isupper())
def test_split(self):
s = 'hello world'
self.assertEqual(s.split(), ['hello', 'world'])
# check that s.split fails when the separator is not a string
with self.assertRaises(TypeError):
s.split(2)
if __name__ == '__main__':
unittest.main() # 测试接口,可以执行上面的几个测试方法
# unittest.main(verbosity=2) # verbosity是测试结果的详细程度
继承 unittest.TestCase
就创建了一个测试样例。上述三个独立的测试是三个类的方法,这些方法的命名都以 test
开头。 这个命名规则是为了告诉测试人员哪些属于测试内容。
每个测试的关键是:调用 assertEqual()
来检查预期的输出; 调用 assertTrue()
或 assertFalse()
来验证一个条件;调用 assertRaises()
来验证抛出了一个特定的异常。使用这些方法而不是 assert
语句是为了让测试运行者能聚合所有的测试结果并产生结果报告。
最后的代码块中,演示了运行测试的一个简单的方法。 unittest.main()
提供了一个测试脚本的命令行接口
在命令行运行该测试脚本,上文的脚本生成如以下格式的输出:
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
在调用测试脚本时添加 -v
参数使 unittest.main()
显示更为详细的信息,生成如以下形式的输出:
PS C:\Users\...\Desktop> python -m unittest -v test.TestStringMethods.test_upper
test_upper (test.TestStringMethods) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
PS C:\Users\...\Desktop> python -m unittest -v test
test_isupper (test.TestStringMethods) ... ok
test_split (test.TestStringMethods) ... ok
test_upper (test.TestStringMethods) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.001s
OK
命令行界面
unittest 模块可以通过命令行运行模块、类和独立测试方法的测试:
python -m unittest test_module1 test_module2 # 同时执行两个模块
python -m unittest test_module.TestClass # 执行某个模块的测试类
python -m unittest test_module.TestClass.test_method # 执行模块下类的某个测试方法
python -m unittest tests/test_something.py # 通过文件路径执行
譬如上面我们写的 test.py
文件,可以这么执行:
python -m unittest test.TestStringMethods.test_upper # 只执行 test_upper 这个测试方法
python -m unittest test.TestStringMethods # 只执行 test.py 里面的 TestStringMethods 类的所有测试方法
python -m unittest test # 注意,test后面没有 .py ,这句话会执行 test.py 里面的所有测试方法
python -m unittest test.py
参数:
-
-v:获取更详细的测试信息
-
-b:buffer。在测试运行时,标准输出流与标准错误流会被放入缓冲区。成功的测试的运行时输出会被丢弃;测试不通过时的输出会正常显示,错误会被加入到测试失败信息。
-
-c:catch。当测试正在运行时, Control-C 会等待当前测试完成,并在完成后报告已执行的测试的结果。当再次按下 Control-C 时,引发平常的
KeyboardInterrupt
异常 -
-f:failfast。出现第一个错误或失败时,停止运行
-
-k:运行匹配模式的测试方法或测试类。可以多次使用这个参数,来匹配所有想要运行的测试。匹配是大小写敏感的。例如,
-k foo
可以匹配到foo_tests.SomeTest.test_something
和bar_tests.SomeTest.test_foo
,但是不能匹配到bar_tests.FooTest.test_something
。*
匹配所有字符,?
匹配单个字符。 -
-h:获取帮助
-
无:不加参数,进行探索性测试
python -m unittest -v test.py
python -m unittest -h
python -m unittest
python -m unittest -v -k test.Te*
python -m unittest -v -k test.TestStringMethods.*_split
探索性测试
Unittest支持简单的测试搜索。若需要使用探索性测试,所有的测试文件必须是 modules 或 packages (包括 namespace packages ),并可从项目根目录导入(即它们的文件名必须是有效的 identifiers )。
探索性测试在 TestLoader.discover()
中实现,但也可以通过命令行使用。它在命令行中的基本用法如下:
cd project_directory
python -m unittest discover # 等同于 python -m unittest
方便起见, python -m unittest
与 python -m unittest discover
等价。如果你需要向探索性测试传入参数,必须显式地使用 discover
子命令。
参数
-v,--verbose:更详细的结果
-s,--start-directory directory:开始搜索的目录(默认当前目录)
-p,--pattern pattern:匹配的测试文件模式(默认 test*.py)
-t,--top-level-directory directory:指定项目的最顶层目录(通常是开始时的目录)
-s
,-p
和 -t
选项可以按顺序作为位置参数传入。以下两条命令是等价的:
python -m unittest discover -s project_directory -p "*_test.py"
python -m unittest discover project_directory "*_test.py"
警告
探索性测试通过导入测试对测试进行加载。在找到所有你指定的开始目录下的所有测试文件后,它把路径转换为包名并进行导入。如 foo/bar/baz.py
会被导入为 foo.bar.baz
。
组织你的测试代码
前置条件:setUp() ,环境还原:tearDown()
当每个测试用例都用到了同样的前置操作,我们可以把前置条件单独拿出来,写成 setUp() 。在运行测试时,测试框架会自动地为每个单独测试调用前置方法。
同样的,测试框架会自动为每个测试用例执行 tearDown() 方法进行环境清理工作。
注意:
- 如果 setUp() 执行失败,测试框架会认为测试发生了错误,因此测试方法不会被运行!
- 多个测试运行的顺序由内置字符串排序方法对测试名进行排序的结果决定。
- 若
setUp()
成功运行,无论测试方法是否成功,都会运行tearDown()
。
这样的一个测试代码运行的环境被称为 test fixture 。一个新的 TestCase 实例作为一个测试脚手架,用于运行各个独立的测试方法。在运行每个测试时,setUp()
、tearDown()
和 __init__()
会被调用一次。
test.py
import unittest
class TestStringMethods(unittest.TestCase):
def setUp(self): # 每个测试用例执行前,都会执行一次此方法
self.string = 'FOO'
def test_upper(self):
self.assertEqual(self.string,'FOO')
def test_lower(self):
self.assertEqual(self.string,'foo')
def tearDown(self): # 每个测试用例执行后,都会执行一次
del self.string
if __name__ == '__main__':
unittest.main(verbosity=2)
测试套件:suite
为了将测试 TestCase 集合打包起来。unittest
为此提供了机制:test suite,以 unittest
的类 TestSuite
为代表,suite 就是测试用例的集合体,实现测试用例的批量运行。大部分情况下,调用 unittest.main()
即可,并且它会为你集合所有模块的测试用例并执行。官方建议单独写在一个文件里,可以单独运行。
import unittest
from test import TestStringMethods # 导入待测试的类
def suite():
suite = unittest.TestSuite() # 测试套件
suite.addTest(TestStringMethods('test_upper')) # 添加测试,addTest(类名('测试方法名'))
suite.addTest(TestStringMethods('test_lower'))
return suite
if __name__ == '__main__':
runner = unittest.TextTestRunner() # 文本类型的测试运行器
runner.run(suite())
跳过测试:
# 装饰器
@unittest.skip(message) # 跳过测试用例,显示跳过的信息
@unittest.skipIf(condition,message) # 如果条件成立,跳过测试,显示信息
@unittest.skipUnless(condition,message) # 如果条件成立,则不跳过,显示信息
# 方法内部
self.skipTest(message) # 方法内部跳过
示例:
import unittest
# @unittest.skip("demonstrating skipping") # 也可以跳过类
class MyTestCase(unittest.TestCase):
@unittest.skip("demonstrating skipping")
def test_nothing(self):
self.fail("shouldn't happen")
@unittest.skipIf(2>1,"2 is biger than 1")
def test_format(self):
# Tests that work for only a certain version of the library.
pass
@unittest.skipUnless(2==2, "2 is equal 2")
def test_windows_support(self):
self.assertEqual(2,2,'2 is equal 3')
pass
def test_maybe_skipped(self):
self.skipTest("internal skip")
self.assertEqual(2,2,'not equal')
# test code that depends on the external resource
pass
unittest.main(verbosity=2)
结果:
D:\Software\python3.8\python.exe C:/Users/.../Desktop/oterh.py
test_format (__main__.MyTestCase) ... skipped '2 is biger than 1'
test_maybe_skipped (__main__.MyTestCase) ... skipped 'internal skip'
test_nothing (__main__.MyTestCase) ... skipped 'demonstrating skipping'
test_windows_support (__main__.MyTestCase) ... ok
----------------------------------------------------------------------
Ran 4 tests in 0.000s
预计失败
@unittest.expectedFailure
可以预计测试用例的失败,即:如果测试用例执行失败,才说明是对的,是成功的;反之如果测试用例通过了,反而说明出问题了。
import unittest
class MyTestCase(unittest.TestCase):
@unittest.expectedFailure
def test_windows_support(self):
self.assertEqual(2,3,'2 is equal 3')
pass
unittest.main(verbosity=2)
结果:
D:\Software\python3.8\python.exe C:/Users/.../Desktop/oterh.py
test_windows_support (__main__.MyTestCase) ... expected failure
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK (expected failures=1)
测试迭代:subTest()
当测试用例只有非常小的差别时,可以用subTest 这个上下文管理器,来执行测试
import unittest
class NumbersTest(unittest.TestCase):
def test_even(self):
"""
Test that numbers between 0 and 5 are all even.
"""
for i in range(0, 6):
with self.subTest(i=i):
self.assertEqual(i % 2, 0)
unittest.main(verbosity=2)
如果不用subTest这个上下文管理器,测试遇到第一个失败就会停止,难以定位错误。
结果:
D:\Software\python3.8\python.exe C:/Users/.../Desktop/oterh.py
test_even (__main__.NumbersTest) ...
======================================================================
FAIL: test_even (__main__.NumbersTest) (i=1)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:/Users/.../Desktop/oterh.py", line 11, in test_even
self.assertEqual(i % 2, 0)
AssertionError: 1 != 0
======================================================================
FAIL: test_even (__main__.NumbersTest) (i=3)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:/Users/.../Desktop/oterh.py", line 11, in test_even
self.assertEqual(i % 2, 0)
AssertionError: 1 != 0
======================================================================
FAIL: test_even (__main__.NumbersTest) (i=5)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:/Users/.../Desktop/oterh.py", line 11, in test_even
self.assertEqual(i % 2, 0)
AssertionError: 1 != 0
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=3)
Process finished with exit code 1
常用的断言
每个断言方法都有一个参数:msg,会当做失败时的错误信息。
Method | Checks that | New in |
---|---|---|
assertEqual(a, b) |
a == b |
|
assertNotEqual(a, b) |
a != b |
|
assertTrue(x) |
bool(x) is True |
|
assertFalse(x) |
bool(x) is False |
|
assertIs(a, b) |
a is b |
3.1 |
assertIsNot(a, b) |
a is not b |
3.1 |
assertIsNone(x) |
x is None |
3.1 |
assertIsNotNone(x) |
x is not None |
3.1 |
assertIn(a, b) |
a in b |
3.1 |
assertNotIn(a, b) |
a not in b |
3.1 |
assertIsInstance(a, b) |
isinstance(a, b) |
3.2 |
assertNotIsInstance(a, b) |
not isinstance(a, b) |
3.2 |
特殊:
Method | Checks that | New in |
---|---|---|
assertAlmostEqual(a, b) |
round(a-b, 7) == 0 |
|
assertNotAlmostEqual(a, b) |
round(a-b, 7) != 0 |
|
assertGreater(a, b) |
a > b |
3.1 |
assertGreaterEqual(a, b) |
a >= b |
3.1 |
assertLess(a, b) |
a < b |
3.1 |
assertLessEqual(a, b) |
a <= b |
3.1 |
assertRegex(s, r) |
r.search(s) |
3.1 |
assertNotRegex(s, r) |
not r.search(s) |
3.2 |
assertCountEqual(a, b) |
a and b have the same elements in the same number, regardless of their order. | 3.2 |
列表,字典等:
Method | Used to compare | New in |
---|---|---|
assertMultiLineEqual(a, b) |
strings | 3.1 |
assertSequenceEqual(a, b) |
sequences | 3.1 |
assertListEqual(a, b) |
lists | 3.1 |
assertTupleEqual(a, b) |
tuples | 3.1 |
assertSetEqual(a, b) |
sets or frozensets | 3.1 |
assertDictEqual(a, b) |
dicts | 3.1 |
常用方法:
TestCase 类:
setUp(), tearDown()
见上文。
setUpClass() 、 tearDownClass()
和 setUp(),tearDown() 类似,只不过 setUp() 是针对每个测试方法,而 setUpClass() 则针对 类 。它会在类里所有的测试方法执行前执行,并且一定要加装饰器:@classmethod,同理 tearDownClass() 也一样。
import unittest
class Test(unittest.TestCase):
@classmethod
def setUpClass(cls):
print('This is SetUpClass')
@classmethod
def tearDownClass(cls):
print('This is TearDownClass')
addCleanup(),doCleanUp()
addCleanup() 可以加载一些函数,doCleanUp() 会执行加载的这些函数。
addCleanUp() 此方法通过添加其他函数,然后运行这些函数,来进行测试环境的清理,和 tearDown() 的用途一样。但是不同的是,哪怕 setUp() 执行失败,此方法里面添加的函数依然会在测试用例结束后执行。(如果 setUp 执行失败,tearDown 不会执行,但是 doCleanUp 会执行)。需要注意的是,addCleanUp 添加的这些函数,执行时是有顺序的,遵循 LIFO 原则,即先入后出:先添加的函数后执行。
doCleanUp() 不需要手动调用,它在测试用例的 tearDown 函数后面执行。当然如果手动调用了,那会在调用的时候执行。
import unittest
class Test(unittest.TestCase):
def del_item(self):
print('clean is doing')
del self.name
def add_item(self,name):
self.name = name
def test_1(self):
self.name = 'test_1'
self.addCleanup(self.add_item, name='wang') # 添加了函数 add_item,name='' 是传递给 add_item 函数的参数。
self.addCleanup(self.del_item) # 添加了del_item 函数,没有参数
self.doCleanups() # 手动调用,完成环境清理,执行顺序:del_item, add_item
print(self.name)
self.assertIsInstance(self.name,str)
if __name__ == '__main__':
unittest.main(verbosity=2)
fail()
fail(msg=''),fail 主动引起失败异常,可以直接中断测试用例,让测试失败。
def test_2(self):
self.fail(msg='test_2 is failed') # 主动引发失败
self.name = 'test_2'
print(self.name)
self.assertIsInstance(self.name, str)
TestLoader 类
四种加载方法,可以从模块,字符串等地方加载测试用例,大同小异,直接看示例吧:
test.py 模块
import unittest
class Test(unittest.TestCase):
def test_1(self):
self.name = 'test_1'
self.assertIsInstance(self.name,str)
def test_2(self):
self.name = 'test_2'
self.assertIsInstance(self.name, str)
loader.py
import unittest
import test # 导入 test.py 模块
from test import Test # 导入 Test 类
loader = unittest.TestLoader()
# 1. 模块
suite = loader.loadTestsFromModule(test) # 直接从模块导入测试用例,会加载模块里面所有的测试
# 2.字符串名字导入,格式有:模块; 模块.测试类; 模块.测试类.测试方法
suite = loader.loadTestsFromName('test') # 导入整个模块的测试
suite = loader.loadTestsFromName('test.Test') # 只加载模块里 Test 类的测试方法
suite = loader.loadTestsFromName('test.Test.test_1') # 只加载 test_1 这个测试方法
# 3. 从字符串名字批量导入
suite = loader.loadTestsFromNames(['test','test.Test.test_1']) # 批量导入,参数必须是可迭代对象,如列表,集合
# 4. 从测试用例导入
suite = loader.loadTestsFromTestCase(Test) # 用测试类导入,此处是类名,不是类的实例化对象
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
TestSuite 类
addTest(), addTests()
加载测试用例,前者添加单个。后置添加多个。
test.py 模块
import unittest
class Test(unittest.TestCase):
def test_1(self):
self.name = 'test_1'
self.assertIsInstance(self.name,str)
def test_2(self):
self.name = 'test_2'
self.assertIsInstance(self.name, str)
loader.py
import unittest
from test import Test # 从 test.py 模块中导入 Test 类
loader = unittest.TestLoader()
suite = unittest.TestSuite()
suite.addTest(Test("test_1")) # 添加单个测试
suite.addTests([Test("test_1"),Test('test_2')]) # 添加多个,参数是 可迭代对象
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
TextTestRunner 类
stream
stream可以重定向测试的结果,默认是None,会将结果显示在屏幕上。可以重定向到文件,实现测试结果的保存。
import unittest
class Test(unittest.TestCase):
def test_1(self):
self.name = 'test_1'
self.assertIsInstance(self.name,str)
def test_2(self):
self.name = 'test_2'
self.assertIsInstance(self.name, str)
suite = unittest.TestSuite()
suite.addTests([Test("test_1"),Test('test_2')])
with open('result.txt','w',encoding='utf-8') as f:
runner = unittest.TextTestRunner(stream=f,verbosity=2) # 重定向测试结果
runner.run(suite)
run()
run 方法接受参数:test。也就是测试用例对象,或者suite对象,它可以直接运行这些测试。
import unittest
class Test(unittest.TestCase):
def test_1(self):
self.name = 'test_1'
self.assertIsInstance(self.name,str)
def test_2(self):
self.name = 'test_2'
self.assertIsInstance(self.name, str)
suite = unittest.TestSuite()
suite.addTests([Test("test_1"),Test('test_2')])
runner = unittest.TextTestRunner(verbosity=2) # TestRunner 对象
runner.run(suite) # 运行suite
其他:
TestSuite 对象:
suite可以添加测试用例,是测试用例的一个集合,相当于把测试用例打包批量运行。当运行 suite 时: suite(),会自动运行 run(result) 方法,run 方法会去遍历 suite 中所有的 testcase 并执行testcase(Testcase 对象执行时会自动执行 testcase 自己的 run(result) 方法,将测试结果放在 result 中),每个 testcase 的测试结果都放进参数 result 。suite 对象会给 suite 中的所有测试用例自动分配一个 result 对象来保存结果,所以实际使用中不需要手动传递 result 。
Testcase 对象:
也就是测试用例,它有一个run(result) 方法,接收一个参数 result,将测试结果放在 result 中。
Testloader 对象:
TestLoader 顾名思义,就是一个测试用例加载器,它可以从 模块,名字,测试用例等方面加载测试用例,加载完成后,会返回一个 Testsuite 对象,所以,TestLoader 其实和 suite 类似,只不过搜集加载测试用例的环节更加方便 (suite需要手动一个个添加,或者放到列表添加,不够灵活)。
TestResult 对象:
一个 result 对象保存测试用例的结果:多少fail,多少success,多少error,各种测试结果信息等。
TextTestRunner 对象:
一个运行器,有个 run(test) 方法,可以直接运行 testcase或testsuite ,并将结果保存到 TextTestResult 中。