6.1 基础知识
和你了解的也许不同,在Python中编写和运行单元测试是非常简单的。它不但不会干扰或者破坏现有程序,还会极大地帮助你和其他开发人员维护软件。
测试应该保存在应用程序或库的tests子模块中。这可以使测试代码随模块一同分发,以便只要软件被安装了,它们就可以被任何其他人运行或重用而无需使用源代码包。同时,这也可以避免这些测试代码被错误地安装在顶层tests模块。
通常比较简单的方式是采用模块树的层次结构作为测试树的层级结构。也就是说,覆盖代码mylib/foobar.py的测试应该存储在mylib/tests/test_foobar.py中,这样在查找与某个特定文件相关联的测试时会比较方便,如示例6.1所示。
示例6.1 test_true.py中的一个真实的简单测试
def test_true(): assert True
这是能够写出来的最简单的单元测试。要运行它,只需加载test_true.py文件并运行其中定义的test_true函数。
显然,对于你的所有测试文件都这么做肯定是太痛苦了。这就是nose(https://nose. readthedocs.org/en/latest/)这个包要解决的—安装之后,它将提供nosetests命令,该命令会加载所有以test_开头的文件,然后执行其中所有以test_开头的函数。
因此,针对我们的源代码树中的test_true.py文件运行nosetests将得到以下结果:
$ nosetests -v test_true.test_true ... ok --------------------------------------------------------- Ran 1 test in 0.003s OK
但是,一旦测试失败,输出就会相应改变,以体现这次失败,包括完整的跟踪回溯。
% nosetests -v test_true.test_true ... ok test_true.test_false ... FAIL ========================================================= FAIL: test_true.test_false Traceback (most recent call last): File "/usr/lib/python2.7/dist-packages/nose/case.py", line 197, in runTest self.test(*self.arg) File "/home/jd/test_true.py", line 5, in test_false assert False AssertionError --------------------------------------------------------- Ran 2 tests in 0.003s FAILED (failures=1)
一旦有AssertionError异常抛出,测试就失败了;一旦assert的参数被判断为某些假值(False、None、0等),它就会抛出AssertionError异常。如果有其他异常抛出,测试也会出错退出。
很简单,对吗?这种方法尽管简单,但却在很多小的项目中广泛使用且工作良好。除了nose,它们不需要其他工具或库,而且只依赖assert就足够了。
不过,在需要编写更复杂的测试时,只使用assert会让人很抓狂。设想一下下面这个测试:
def test_key(): a = ['a', 'b'] b = ['b'] assert a == b
当运行nosetests时,它会给出如下输出:
$ nosetests -v test_complicated.test_key ... FAIL ========================================================== FAIL: test_complicated.test_key Traceback (most recent call last): File "/usr/lib/python2.7/dist-packages/nose/case.py", line 197, in runTest self.test(*self.arg) File "/home/jd/test_complicated.py", line 4, in test_key assert a == b AssertionError --------------------------------------------------------- Ran 1 test in 0.001s FAILED (failures=1)
显然,因为a和b不同,所以测试不能通过。但是,它们到底有何不同呢?assert没有给出这一信息,而只是声称此断言是错误的—这是没什么用的。
而且,用这种基本的无框架方式实现一些高级的测试(如忽略某个测试,或者在每个测试之前或之后执行某些操作)也会非常痛苦。
这用unittest就比较方便了。它提供了解决上述问题的工具,而且unittest是Python标准库的一部分。
警告
unittest在Python 2.7中已经做了较大改进,如果正在支持Python的早期版本,那么可能需要使用它的向后移植的名字unittest2(https://pypi.python.org/pypi/unittest2/)。如果需要支持Python 2.6,可以使用下面的代码段在运行时为任何Python版本导入正确的模块:
try: import unittest2 as unittest except ImportError: import unittest
如果用unittest重写前面的例子,看起来会是下面的样子:
import unittest class TestKey(unittest.TestCase): def test_key(self): a = ['a', 'b'] b = ['b'] self.assertEqual(a, b)
如你所见,实现起来并没有更复杂。需要做的就只是创建一个继承自unittest.TestCase的类,并且写一个运行测试的方法。与使用assert不同,我们依赖unittest.TestCase类提供的一个方法,它提供了一个等价的测试器。运行时,其输出如下:
$ nosetests -v test_key (test_complicated.TestKey) ... FAIL ========================================================= FAIL: test_key (test_complicated.TestKey) Traceback (most recent call last): File "/home/jd/Source/python-book/test_complicated.py", line 7, in test_key self.assertEqual(a, b) AssertionError: Lists differ: ['a', 'b'] != ['b'] First differing element 0: a b First list contains 1 additional elements. First extra element 1: b - ['a', 'b'] + ['b'] --------------------------------------------------------- Ran 1 test in 0.001s FAILED (failures=1)
如你所见,这个输出结果很有用。仍然有断言错误被抛出,而且测试仍然失败了,但至少我们获得了为什么测试会失败的真正信息,它可以帮我们解决这个问题。这就是写测试用例时永远不应该使用assert的原因。任何人试图hack你的代码并最终遇到某个测试失败时都会感谢你没有使用assert,这同时也为他提供了调试信息。
unittest提供了一组测试函数,可以用来特化测试,如assertDictEqual、ass-ertEqual、assertTrue、assertFalse、assertGreater、 assertGreaterEqual、assertIn、assertIs、assertIsIntance、assertIsNon、 assertualIsNot、as-sertIsNotNone、assertItemsEqual、assertLess、assertLessEqual、 asse-rtListEqual、assertMultiLineEqual、assertNotAlmostEqual、assertNot-Equal、 assertTupleEqual、assertRaises、assertRaisesRegexp、assertReg-expMatches等。最好是通读一遍pydoc unittest,以便全面了解。
也可以使用fail(msg)方法有意让某个测试立刻失败。例如,已知代码的某个部分如果执行一定会抛出一个错误但没有特定的断言去检查时,这是很方便的,如示例6.2所示。
示例6.2 让测试失败
import unittest class TestFail(unittest.TestCase): def test_range(self): for x in range(5): if x > 4: self.fail("Range returned a too big value: %d" % x)
有时候,某个测试如果不能运行,忽略它是很有用的。例如,希望根据某个库的存在与否有条件地运行某个测试。为此,可以抛出unitest.SkipTest异常。当该测试被执行时,它只是被简单地标注为已忽略。更便利的方法是使用unittest.TestCase.skipTest()而不是手工抛出这一异常,另外也可以使用unittest.skip装饰器,如示例6.3所示。
示例6.3 忽略测试
import unittest try: import mylib except ImportError: mylib = None class TestSkipped(unittest.TestCase): @unittest.skip("Do not run this") def test_fail(self): self.fail("This should not be run") @unittest.skipIf(mylib is None, "mylib is not available") def test_mylib(self): self.assertEqual(mylib.foobar(), 42) def test_skip_at_runtime(self): if True: self.skipTest("Finally I don't want to run it")
执行后,该测试文件会输出下列内容:
$ python -m unittest -v test_skip test_fail (test_skip.TestSkipped) ... skipped 'Do not run this' test_mylib (test_skip.TestSkipped) ... skipped 'mylib is not available' test_skip_at_runtime (test_skip.TestSkipped) ... skipped "Finally I don't want to run it" --------------------------------------------------------- Ran 3 tests in 0.000s OK (skipped=3)
提示
在示例6.3中你可能已经注意到,unittest模块提供了一种执行包含测试的Python模
块的方式。它没有nosetests那么方便,因为它不会发现自己的测试文件,但它对于运行特定测试模块仍然是很有用的。
在许多场景中,需要在运行某个测试前后执行一组通用的操作。unittest提供了两个特殊的方法setUp和tearDown,它们会在类的每个测试方法调用前后执行一次,如示例6.4所示。
示例6.4 使用unittest的setUp方法
import unittest class TestMe(unittest.TestCase): def setUp(self): self.list = [1, 2, 3] def test_length(self): self.list.append(4) self.assertEqual(len(self.list), 4) def test_has_one(self): self.assertEqual(len(self.list), 3) self.assertIn(1, self.list)
在这个示例中,setUp会在运行test_length和test_has_one之前被调用。它可以非常方便地创建在每个测试中要用到的对象,但你需要保证它们在运行每个测试之前,在干净的状态下被重建。这对于创建测试环境是非常有用的,经常被称为fixture(参见6.2节)。
提示
使用nosetests时,经常会只想运行某个特定的测试。你可以选择要运行的测试作为参数,语法是path.to.your.module:ClassOfYourTest.test_method。确保在模块路径和类名之前有一个冒号。也可以指定path.to.your.module:ClassOfYourTest来执行整个类,或者指定path.to.your.module来执行整个模块。
提示
通过同时运行多个测试可以加快速度。只需为nosetests调用加上--process=N选项即可创建多个nosetests进程。不过,testrepository是更好的选择(这会在6.5节中讨论)。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论