Python单元测试-unittest
unittest作为一个python中的基本模块,是其他框架和工具的基础,官方文档神马的最实用了:https://docs.python.org/2/library/unittest.html
对官方文档做了一个粗略的翻译,稍有调整。
python版本:2.7
基本概念
- test fixture
一个test fixture代表执行一个或者多个测试时需要准备环境,以及相关联的清理环境的工作。这包含很多内容,比如创建临时的数据库、目录等。 - test case
一个test case就是测试用例,测试当中的最小单元。unittest提供一个基本的类TestCase,用来创建一个test case。 - test suite
test suite是一组test case或者test suite的集合,也可以两者都有。用来将需要一同执行的测试用例聚合到一起。 - test runner
一个test runner是用来执行测试用例的,对测试进行编排并把结果返回给用户。
一个TestCase的实例应该是完全独立的,可以独立的运行测试,也可以和其他测试用例进行组合。最简单的TestCase子类简单的重写runTest()方法执行特定代码就可以:
import unittest
class JustForTest(unittest.TestCase):
def runTest(self):
length = 10
self.assertEqual(10, length)
通过unittest模块可以在命令行下对一个模块、一个类或者是一个方法执行测试,比如对这个test.py模块执行单元测试:
➜ python -m unittest test
.
---------------------------------------------------------
Ran 1 test in 0.000s
OK
#
一个简单用例
unittest模块为构建和执行测试提供了非常丰富的工具集,下面这个例子用来测试三个字符串的方法:
import unittest
class TestStringMethods(unittest.TestCase):
def test_upper(self):
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'])
with self.assertRaises(TypeError):
s.split(2)
if __name__ == '__main__':
unittest.main()
测试用例是unittest.TestCase的子类,三个独立的测试是以test开头的。用这样的命名规则来约定哪些方法是test runner需要执行的。
每个测试的关键在于调用assertEqual()进行检查期望的结果;assertTrue()和assertFalse()用来判断一个条件;assertRaises()用来确认抛出了一个指定的异常。这些方法用来代替assert语句,test runner可以收集所有的结果并生成最后的报告。
代码总13~14行是一个简单的用来执行测试的方式。unittest.main()给测试脚本提供了一个命令行接口。当从命令行执行的时候,上面的脚本会有如下输出:
➜ python basic_example.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
不使用unittest.main()的话也可以用其他方式替代,比如13、14行可以替换为:
#if __name__ == '__main__':
# unittest.main()
suite = unittest.TestLoader().loadTestsFromTestCase(TestStringMethods)
unittest.TextTestRunner(verbosity=2).run(suite)
可以得到更加清晰的输出:
➜ python_unit_test_study python basic_example.py
test_isupper (__main__.TestStringMethods) ... ok
test_split (__main__.TestStringMethods) ... ok
test_upper (__main__.TestStringMethods) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
命令行接口
unittest模块可以从命令行执行模块中的测试,可以是一个类也可以使单独的测试用例:
➜ python -m unittest basic_example.TestStringMethod$
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
➜ python_unit_test_study python -m unittest basic_example.TestStringMethods
.test_isupper
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
也可以传递一个模块列表,执行多个模块中的测试用例:
➜ python -m unittest test_module1 test_module2
-v参数可以获得更多的内容:
➜ python -m unittest -v basic_example
test_isupper (basic_example.TestStringMethods) ... ok
test_split (basic_example.TestStringMethods) ... ok
test_upper (basic_example.TestStringMethods) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
-h可以获得完整命令行参数的帮助说明:
➜ python -m unittest -h
Usage: python -m unittest [options] [tests]
Options:
-h, --help Show this message
-v, --verbose Verbose output
-q, --quiet Minimal output
-f, --failfast Stop on first failure
-c, --catch Catch control-C and display results
-b, --buffer Buffer stdout and stderr during test runs
Examples:
python -m unittest test_module - run tests from test_module
python -m unittest module.TestClass - run tests from module.TestClass
python -m unittest module.Class.test_method - run specified test method
[tests] can be a list of any number of test modules, classes and test
methods.
Alternative Usage: python -m unittest discover [options]
Options:
-v, --verbose Verbose output
-f, --failfast Stop on first failure
-c, --catch Catch control-C and display results
-b, --buffer Buffer stdout and stderr during test runs
-s directory Directory to start discovery ('.' default)
-p pattern Pattern to match test files ('test*.py' default)
-t directory Top level directory of project (default to
start directory)
For test discovery all test modules must be importable from the top
level directory of the project.
测试发现
unittest支持非常简单的测试发现。为了兼容测试发现,所有的测试文件(test_xxx.py)必须是可从项目的顶级目录导入的模块或包。测试发现由TestLoader.discover()实现,但是可以通过命令行使用,基本的用法如下:
➜ python -m unittest discover
.....
----------------------------------------------------------------------
Ran 5 tests in 0.000s
OK
➜ python_unit_test_study python -m unittest discover -h
Usage: python -m unittest discover [options]
Options:
-h, --help show this help message and exit
-v, --verbose Verbose output
-f, --failfast Stop on first fail or error
-c, --catch Catch Ctrl-C and display results so far
-b, --buffer Buffer stdout and stderr during tests
-s START, --start-directory=START
Directory to start discovery ('.' default)
-p PATTERN, --pattern=PATTERN
Pattern to match tests ('test*.py' default)
-t TOP, --top-level-directory=TOP
Top level directory of project (defaults to start
directory)
组织代码
单元测试最基本的结构应该是测试用例——必须设置并检查正确性的单独场景。在unittest中,测试用例就是unittest的TestCase类的一个实例。写测试代码时,必须书写TestCase的子类,或者使用FunctionTestCase。
文章开头讲过,最基本的TestCase子类就是重写runTest()方法即可,在runTest()中执行响应的测试代码。比如:
import unittest
class DefaultWidgetSizeTestCase(unittest.TestCase):
def runTest(self):
widget = Widget('The widget')
self.assertEqual(widget.size(), (50, 50), 'incorrect default size')
为了执行测试内容,使用TestCase基类提供的assert*()中的方法来检查结果。如果测试失败,就会抛出异常,unittest会将这个测试标记为Failure。而其他的异常都会被当做Error处理。这可以帮助我们判断代码中的问题:failure是由于测试结果引起的错误-期望值是5得到的却是6。而Errors是由于代码本身的错误引起,比如常见的TypeError等。
当我们对同一类型的测试内容写测试代码时,很多测试方法的构建和初始化可能是重复的,这种情况下我们可以使用setUp()方法,setUp()可以将初始化统一抽离出来,在一个测试方法执行前都会先执行,比如:
如果setUp()方法抛出异常的话,测试框架认为测试过程遇到了错误,runTest()方法不会被执行的。
import unittest
class SimpleWidgetTestCase(unittest.TestCase):
def setUp(self):
self.widget = Widget("The widget")
class DefaultWidgetSizeTestCase(SimpleWidgetTestCase):
def runTest(self):
self.assertEqual(self.widget.size(), (50, 50),
'incorrect default size')
class WidgetResizeTestCase(SimpleWidgetTestCase):
def runTest(self):
self.widget.resize(100, 150)
self.assertEqual(self.widget.size(), (100, 150),
'wrong size after resize')
执行结果:
➜ python -m unittest test_widget
EE
======================================================================
ERROR: runTest (test_widget.DefaultWidgetSizeTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_widget.py", line 5, in setUp
self.widget = Widget("The widget")
NameError: global name 'Widget' is not defined
======================================================================
ERROR: runTest (test_widget.WidgetResizeTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_widget.py", line 5, in setUp
self.widget = Widget("The widget")
NameError: global name 'Widget' is not defined
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (errors=2)
同样的,我们可以使用tearDown()方法在runTest()执行结束后进行清理,如果setUp()方法执行成功,不论runTest()是否成功,tearDown()都会执行。
这样一个初始化、清理的完整的测试环境叫做fixture,通常很多小的测试使用的是相同的fixture的,
import unittest
class WidgetTestCase(unittest.TestCase):
def setUp(self):
self.widget = Widget('The widget')
def tearDown(self):
self.widget.dispose()
self.widget = None
def test_default_size(self):
self.assertEqual(self.widget.size(), (50, 50),
'incorrect default size')
def test_resize(self):
self.widget.resize(100, 150)
self.assertEqual(self.widget.size(), (100, 150),
'wrong size after resize')
这里没有使用runTest()方法,但是使用了其他两个测试方法作为替代。测试类实例会执行每一个test_*()方法,self.widget会对每一个实例进行创建和销毁。这种构建方法,创建实例的时候我们需要具体的指出需要执行的测试方法:
defaultSizeTestCase = WidgetTestCase('test_default_size')
resizeTestCase = WidgetTestCase('test_resize')
unittest提供test suite(测试套件)可以将测试用例的实例很好的按照功能特性进行合理的组织,在unittest中通过TestSuite类实现:
widgetTestSuite = unittest.TestSuite()
widgetTestSuite.addTest(WidgetTestCase('test_default_size'))
widgetTestSuite.addTest(WidgetTestCase('test_resize'))
为了方便测试,在每一个模块中提供一个可调用的预构建的test suite是一个不错的主意:
def suite():
suite = unittest.TestSuite()
suite.addTest(WidgetTestCase('test_default_size'))
suite.addTest(WidgetTestCase('test_resize'))
return suite
也可以这样写:
def suite():
tests = ['test_default_size', 'test_resize']
return unittest.TestSuite(map(WidgetTestCase, tests))
由于使用相似名字的测试函数来创建一个TestCase子类是非常通用的模式,unittest提供了一个TestLoader类可以自动的创建测试套件并用独立的测试进行填充,比如:
suite = unittest.TestLoader().loadTestsFromTestCase(WidgetTestCase)
这就创建了一个测试套件,将会执行WidgetTestCase.test_default_size()和WidgetTestCase.test_resize。TestLoader将会自动的识别以test_开头的测试方法。
各个测试的执行顺序是由测试的函数名按照字符串内建顺序执行的。
测试套件本身也可以像测试用例一样组织起来:
suite1 = module1.TheTestSuite()
suite2 = module2.TheTestSuite()
alltests = unittest.TestSuite([suite1, suite2])
我们可以将测试用例与测试套件的定义放在与测试代码相同的模块中,但将测试代码放在单独的模块中又几个好处:
- 测试模块可以独立的通过命令行进行执行
- 测试代码可以更容易的与发布代码分离
- 没有必要的情况下不必为了适应被测试代码而频繁更改测试代码
- 测试的代码更容易重构
。。。等等
跳过测试以及异常测试
unittest支持跳过单独的测试方法甚至是整个测试类。也支持将一个测试标记为“expected failure”。
跳过一个测试可以使用skip()描述符或者它的一个条件语句,最基本的skip使用像这样:
import unittest
import sys
class MyTestCase(unittest.TestCase):
@unittest.skip('demonstrating sipping')
def test_nothing(self):
self.fail("shouldn't happen")
@unittest.skipUnless(sys.platform.startswith("win"), 'requires Windows')
def test_windows_support(self):
pass
执行结果:
➜ python_unit_test_study python -m unittest -v mylib
test_nothing (mylib.MyTestCase) ... skipped 'demonstrating sipping'
test_windows_support (mylib.MyTestCase) ... skipped 'requires Windows'
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK (skipped=2)
如果想跳过整个测试类,方法是和上面是一样的。
期望失败通过expectedFailure()描述符实现的:
@unittest.expectedFailure
def test_format(self):
pass
执行结果:
➜ python -m unittest -v mylib
test_format (mylib.MyTestCase) ... unexpected success
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK (unexpected successes=1)
被跳过的测试不会执行setUp和tearDown,被跳过的类也不会执行setUpClass和tearDownClass。