返回介绍

建议74:为包编写单元测试

发布于 2024-01-30 22:19:09 字数 4519 浏览 0 评论 0 收藏 0

当我们创建了一个包,接着就开始为它编写业务逻辑的代码。比如在前文中,我们创建了arithmetic包,并在里面增加一个加法函数,如下:

def add(x, y):
  return x + y

无名氏说:“当你写下代码,bug随之而来”。所以我们需要对代码进行测试,以便交付物在交付给业务的下游部门使用时有一定质量保障。对于一个函数而言,最简单的方法也许就是为它编写一些单元测试代码了。

if __name__ == "__main__":
  assert add(1, 2) == 3
  assert add(1, -1) == 0

这样,当以arithmetic.py为入口文件执行arithmetic.py的时候,就会运行这些测试代码,实现对add()函数的质量检测。像这种针对函数编写的测试,我们称为“单元测试”,它是白盒测试的一种,所以单元测试用例都是根据函数的代码而制定的。通过单元测试,可以有效地避免软件退化,增进软件质量,并更快地产生健壮的代码。甚至对开发人员来说,单元测试用例也是最好的文档。

虽然上例让大家感觉测试非常简单,但实际项目中的测试也有不少麻烦:

1)程序员希望测试更加自动化,想象一下,如果加减乘除4个函数不是实现在arithemtic.py一个文件中,而是分列在4个文件中,那么要测试它们就需要分别运行这4个文件。再想象一下,实际项目中可能一个包中有几十甚至上百个文件,那么想要全部测试一次就非常困难。

2)一个测试用例往往在测试之前需要进行打桩或做一些准备工作,在测试之后要清理现场,最好有一个框架可以自动完成这些工作。

3)对于大项目,大量的测试用例需要分门别类地放置,而测试之后,分别产生相应的测试报告。

Python是一门务实的语言,所以自带的电池中就包含了一个名为unittest的模块,可以解决这些问题。关于unittest的知识,我们在建议73中已经学过,接下来就看一下如何使用unittest进行测试的代码。

import unittest
import arithmetic
class TestCase(unittest.TestCase):
  def test_add(self):
    self.assertEqual(arithmetic.add(1, 1), 2)
if __name__ == "__main__":
  unittest.main()
把这些代码保存到 test_arithmetic.py 
中,然后执行命令:
>python test_arithmetic.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK

虽然没有显式地调用TestCase.test_add,但从Ran1 test in 0.000s这句输出中可以看到这个测试用例已经执行到了,这就是框架的好处。除了自动调用匹配以test开头的方法之外,unittest.TestCase还有模板方法setUp()和tearDown(),通过覆盖这两个方法,能够实现在测试之前执行一些准备工作,并在测试之后清理现场。

然后再回到最初的假设:在arithmetic项目中,若加减乘除4个函数分别在不同的文件中,那么测试用例也可能分别写在4个文件中,那么运行python test_xxx.py命令的形式就无法简化测试工作。这时候可以使用unittest的测试发现(test discover)功能。

>python -m unittest discover
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK

unittest将递归地查找当前目录下匹配test*.py模式的文件,并将其中unittest.TestCase的所有子类都实例化,然后调用相应的测试方法进行测试,这就一举解决了“通过一条命令运行全部测试用例”的问题。

unittest的测试发现功能是Python 2.7版本中才有的,如果你在使用更旧的版本,请安装unittest2。

尽管unittest的测试发现功能已经非常方便,但是因为它需要高版本的Python支持,所以大家喜欢使用setuptools的扩展命令test。

>python setup.py test 
running test
running egg_info
writing arithmetic.egg-info/PKG-INFO
writing top-level names to arithmetic.egg-info/top_level.txt
writing dependency_links to arithmetic.egg-info/dependency_links.txt
writing entry points to arithmetic.egg-info/entry_points.txt
reading manifest file 'arithmetic.egg-info/SOURCES.txt'
writing manifest file 'arithmetic.egg-info/SOURCES.txt'
running build_ext
test_add (test_arithmetic.TestCase) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK

setuptools对distutils.commands进行了扩展,增加了test命令。如上例所示,这个命令执行的时候,先运行egg_info和build_ext子命令构建项目,然后把项目路径加到sys.path中,再搜寻所有的测试套件(test suite,通常指多个测试用例或测试套件的组合),并运行之。要使用这个扩展命令,需要在调用setup()函数的时候向它传递test_suite元数据。比如在arithmetic项目中,是这样的:

>cat setup.py 
...
setup(name='arithmetic',
...
    test_suite = "test_arithmetic",
...

test_suite元数据的值可以指向一个包、模块、类或函数,比如在著名的flask项目中,是test_suite='flask.testsuite.suite',其中flask.testsuite.suite是一个函数;而在arithmetic项目中,test_arithmetic是一个模块。

使用setuptools的测试发现功能,可以给开发人员更一致的开发体验,就像使用build、install命令一样,所以受到了大家的喜爱。但是来自unittest本身的缺陷让开发人员想要找到一个更好的测试框架。

1)unittest并不够Pythonic,比如从JUnit中继承而来的首字母小写的骆驼命令法;所有的测试用例都需要从TestCase继承。

2)unittest的setUp()和tearDown()只是在TestCase的层面上提供,即每一个测试用例执行的时候都会运行一遍,如果有许多模块需要测试,那么创建环境和清理现场操作都会带来大量工作。

3)unittest没有插件机制进行功能扩展,比如想要增加测试覆盖统计特性就非常困难。

nose就是作为更好的测试框架进入大家视线的,而且它更是一个具有更强大的测试发现运行的程序。此外nose定义了插件机制,使得扩展nose的功能成为可能(默认自带coverage插件)。使用pip、easy_install安装以后,就多了一个nosetests命令可以使用。比如在arithmetic项目中运行:

>nosetests -v
test_add (test_arithmetic.TestCase) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.004s
OK

可以看到nose能够自动发现测试用例,并调用执行,由于它与原有的unittest测试用例兼容,所以可以随时将它引入到项目中来。其实nose的测试发现机制更进一步,它抛弃了unittest中测试用例必须放在TestCase子类中的限制,只要命名符合(?:^|[b_.-])[Tt]est正则表达式的类和函数都可作为测试用例运行。

此外,nose作为一个测试框架,也提供了与unittest.TestCase类似的断言函数,但它抛弃了unittest的那种Java风格的命令方式,使用的是符合PEP8的命名方式。

针对unittest中setUp()和tearDown()只能放在TestCase中的问题,nose提供了3个级别的解决方案,这些配置和清理函数,可以放在包(__init__.py文件中)、模块和测试用例中,非常完美地解决了不同层次的测试需要的配置和清理需求。

最后,nose与setuptools的集成更加友好,提供了nose.collector作为通过的测试套件,让开发人员无须针对不同项目编写不同的套件。比如针对arithmetic项目的setup.py文件作如下修改:

>cat setup.py 
...
setup(name='arithmetic',
...
#    test_suite = "test_arithmetic",
    test_suite = "nose.collector",
...

然后运行python setup.py test,得到的结果是一样的。因为使用了nose.collector之后,test_suite元数据就确定不变了,所以它也非常适合写入paster的模板中去,在构建目录的时候自动生成。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文