Python项目的pytest初始化

为什么要写测试

Python是一种必须要写测试的语言。

和C/C++、Java这种需要编译的语言相比,Python这种动态语言天生就有开发迅速的优势。 开发完成后的代码,跑一跑自然知道问题。 然而,如果项目稍大,就难以保证手动测试的可靠性。 因此,江湖传言,Python不适合做大型项目。 当然,江湖上的大多数人是不写测试的。

与Java之类相比,没有100%测试覆盖的Python代码,甚至都不能保证没有语法错误。 而静态语言在预编译时,就会做完整的语法检查,可靠性的差距可见一斑。

然而,正是因为这种差距,Python项目有更迫切的测试需求。 达成100%测试覆盖率的Python项目,可靠性却又比没有写测试的Java项目要强上许多。

为什么是pytest

打开PyCharm,可以看到默认支持的单元测试框架有五类:

unittest是Python标准库之一。 与Java里著名的JUnit4非常类似,通过继承一个TestCase,然后增加test_*命名的method来测试。 (JUnit5中已经抛弃了这种做法,改为使用注解@Test的形式。) 它的问题就是,有些过于繁琐。

doctest是一种非常奇特的测试技术,在文档中写测试。 把代码和测试写在一起,这是Java这类语言中没有办法做到的。 虽然它也是Python标准库之一,但孤却第一时间放弃了。 对简单的数学函数,它是很便捷的;但对于复杂的、有副作用(side effect)的function或method来说,它功能不足。 而且,测试代码往往比功能代码更复杂些,不合适写在pydoc里。

Twisted是一个异步网络框架,而Trial则是其单元测试系统,是对unittest的一个封装。 如果不是使用Twisted框架,那么不会使用到它。

nose是一个曾经非常流行的测试,兼容unittest的同时却拥有更加简洁的语法,然而已经停止维护了。 nose本身支持Python 3,然而其很多插件却不支持。 新的项目,不推荐使用它,正如它的文档所说。 非常可惜,孤差点就选它了。

Nose has been in maintenance mode for the past several years and will likely cease without a new person/team to take over maintainership. New projects should consider using Nose2, py.test, or just plain unittest/unittest2.

最终,孤选择了pytest,曾用名py.test。 它能兼容unittestnose的测试代码,写法简洁,并且还有自己的独到之处。

在《PythonTestingToolsTaxonomy - Python Wiki》还可看到更多不同类型的测试框架。 本文仅介绍pytest测试最基本的写法,以及如何在一个Python项目进行初始化。

pytest测试样例

unittest类似的是,pytest也需要把测试function或method命名为test_*。 方便之处在于,不仅支持function,测试method所在的class也不需要继承什么。

此外,也不需要assert*系列的function来做检验,直接用内置的assert即可。

def add(a, b):
    return a + b


def test_add():
    assert 3 == add(1, 2)

假如以上代码文件名为add.py,写好后直接运行pytest add.py,即可开始测试。

测试代码结构

孤认为,测试代码不应该随着Python包而发布,尽管很多知名的包做出了相反的选择。 测试应该在打包发布前完成。 而发布后,用户是不需要、也不应该去跑测试的。 很多自带测试的包,甚至会干扰当前项目的测试结果。

而功能代码与测试代码的分离,通常采用以下形式:

project
├── setup.cfg
├── setup.py
├── src
│   └── mypkg
│       ├── __init__.py
│       └── something.py
└── tests
    ├── conftest.py
    ├── test_init.py
    └── test_something.py

源码放在src下,而测试则放在tests下。 不把mypkg直接放在根目录下,是为了避免直接从当前路径导入。 这样虽然方便,但有时会导致安装后的代码的未经测试的(通过测试覆盖率的一些异常可以观察到这一情况)。 详情可以查看这篇著名的博文:《Packaging a python library | ionel’s codelog》,它也是pytest官方文档所推荐的。

tests可以增加__init__.py,成为一个Python包,也可以增加子目录、子包。 conftest.pypytest的默认配置文件,可以在其中放公用的fixture或plugin。

setup.py与setup.cfg

setup.pytests_require中,需要配置pytest。 另外,也建议在setup_requires里配置pytest-runner

from setuptools import setup

setup(
    ...
    setup_requires=[
        'pytest-runner',
    ],
    tests_require=[
        'pytest',
    ],
)

即使不配置pytest,也可直接通过运行pytest命令来测试。 而做出以上配置后,可以通过python setup.py pytest命令来测试,并且不用提前安装pytest

如果需要通过python setup.py test的形式执行测试,则需要添加[aliases]setup.cfg

[aliases]
test=pytest

[tool:pytest]
addopts = --verbose
python_files = tests/*

setup.cfg中的[tool:pytest]块,就是对pytest的配置。 也可在pytest.initox.ini中,添加[pytest]块进行相同配置,效果一样。 不过,配在setup.cfg,可以在项目根目录少一个文件,孤更喜欢一些。

addoptspytest的命令行参数,--verbose会让打印输出更细致。 python_files指定了测试代码的位置。 有了这两个基本的配置,执行测试时就可以不用输入任何参数了。 后续如果有什么新的参数,也都可以配置到这里。

结语

一个项目的pytest初始化就是这样。 然后,就可以开始愉快地写测试了。

参考


相关笔记