pytest中的fixture
2018-01-28 23:53:18 +08 字数:2870 标签: Python Test会写测试,和写好测试,是差距很大的两种境界。 孤大概花了半年时间来破境。 导致这么长时间的壁障,并非是学习难度,而是进入『会写测试』境界后的一种满足心态。
需要被测试内容,如果是没有副作用的数学函数,给固定输入就可以得到特定输出,那么测试样例会很容易写。
然而,『没有副作用』的情况是很少见的,比如print
。
所以,在正式进行测试前,往往需要做些准备;而在测试结束后,可能也需要做些清理。
本文介绍pytest中setup与teardown的写法,算是单元测试的进阶内容吧。
setup与teardown ¶
fixture不太好翻译,大概是『固定装置』、『测试夹具』这类的意思。 如果换一种单元测试常见的称呼,就比较好理解了——setup与teardown,也就是在测试前后,做一些准备和清理。
pytest中,支持setup_*
和teardown_*
形式的function或method,分别在测试样例的前后回调。
共有module、function、class和method四种层级(官方文档中的名词为level,也可理解为作用域scope),大致形式如下。
def setup_module(module):
pass
def teardown_module(module):
pass
def setup_function(function):
pass
def teardown_function(function):
pass
class TestSomething:
@classmethod
def setup_class(cls):
pass
@classmethod
def teardown_class(cls):
pass
def setup_method(self, method):
pass
def teardown_method(self, method):
pass
在pytest的3.0版本以后,上面展示的module
、function
、method
参数,可以去掉。
当然,class的cls
不能去掉。
传参的目的,是支持对这些进行调整,然而大多数情况下是无用的,可以省略。
比如,module的可以写成:
def setup_module():
pass
def teardown_module():
pass
顾名思义,setup_module
就是在同一个module的测试执行前回调一次,teardown_module
则是在之后回调。
与之相比,setup_function
和teardown_function
则是在每次test_*
形式的function被执行的前后回调。
class与method这一组的机制类似。
这种写法比较古老,是为了兼容unittest而保留的,并非pytest推荐的写法。 它的问题是,对需要被setup和teardown的东西,分得不够细致。 假如,有一个资源——比如一个伪造的数据库——需要被3个测试function使用,而这个module共有10个。 按照这种写法,就没有一个简洁优雅的写法,令pytest仅为这3个function准备,而不会影响另外7个。
而pytest独创的fixture写法,就可以完美实现这类场景。
pytest.fixture ¶
import pytest
import smtplib
@pytest.fixture(scope="module")
def smtp():
smtp = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
yield smtp
smtp.close()
def test_ehlo(smtp):
response, msg = smtp.ehlo()
assert response == 250
assert b"smtp.gmail.com" in msg
这是一个官方样例,很明确地展示了fixture用法的各个细节。
首先,@pytest.fixture
作为装饰器(decorator),被它作用的function即可成为一个fixture。
scope="module"
是指定作用域。
类似setup_*
与teardown_*
,这里的scope支持function、class、module、session四种,默认情况下的scope是function。
去掉的method由function替代,而新增的session则是扩大到了整个测试,可以覆盖多个module。
fixture的function名称,可以直接作为参数,传给需要使用它的测试样例。
在使用时,smtp
并非前面定义的function,而是function的返回值,即smtplib.SMTP
。
这一点比较隐晦,稍微违背了Python的哲学(详见《蛇宗三字经》),但却很方便。
yield smtp
当然也可以是return smtp
,不过后面就不能再有语句。
相当于只有setup、没有teardown。
使用yield
,则后面的内容就是teardown。
这样不仅方便,把同一组的预备、清理写在一起,逻辑上也更紧密。
最终,在test_ehlo
中直接声明一个形式参数smtp
,就可以使用这个fixture。
同一个测试function中可以声明多个这类形式参数,也可以混杂其它类型的参数。
如果那些没有使用smtp
这个fixture的function被单独测试,它不会被执行。
另外,在fixture中,也可使用其它fixture作为形式参数,形成树状依赖。 这为测试环境的准备,提供了更高的抽象层级。
conftest.py ¶
前面有提,fixture的scope中,有session,也就是整个测试过程。 这意味着,fixture可以是全局的,供多个module使用。
pytest支持在测试的路径下,存在conftest.py文件,进行全局配置。
tests
├── conftest.py
├── test_a.py
├── test_b.py
└── sub
├── __init__.py
├── conftest.py
├── test_c.py
└── test_d.py
在以上目录结构下,顶层的conftest.py里的配置,可以给四个测试module使用。 而sub下面的conftest.py,只能给sub下面的两个module使用。 如果两个conftest.py中定义了名称相同的fixture,则可以被覆盖; 也就是说,在sub下面的module,使用的是sub下的conftest.py里的定义同名fixture。
内置fixture ¶
以下命令可以列出所有可用的fixture,包括内置的、插件中的、以及当前项目定义的。
pytest --fixtures
其中不乏广泛应用的内容,比如capsys
和tmpdir
。
capsys
Enable capturing of writes to sys.stdout/sys.stderr and make
captured output available via ``capsys.readouterr()`` method calls
which return a ``(out, err)`` tuple. ``out`` and ``err`` will be ``text``
objects.
tmpdir
Return a temporary directory path object
which is unique to each test function invocation,
created as a sub directory of the base temporary
directory. The returned object is a `py.path.local`_
path object.
例如:
def test_print(capsys):
print('hello')
out, err = capsys.readouterr()
assert 'hello' == out
def test_path(tmpdir):
from py._path.local import LocalPath
assert isinstance(tmpdir, LocalPath)
from os.path import isdir
assert isdir(str(tmpdir))
capsys
可以捕捉测试function的标准输出,而tmpdir
则可以自动创建临时文件夹。
它们都是常用fixture,如果没有内置,恐怕所有项目都要自行实现。
Parametrizing ¶
有时候,测试一个function,需要测试多种情况。 而每一种情况的测试逻辑基本雷同,只是参数或环境有异。 这时就需要参数化(Parametrizing)的fixture来减少重复。
比如,前面smtp
那个例子,可能需要准备多个邮箱来测试。
@pytest.fixture(params=["smtp.gmail.com", "mail.python.org"])
def smtp(request):
smtp = smtplib.SMTP(request.param, 587, timeout=5)
yield smtp
print ("finalizing %s" % smtp)
smtp.close()
通过在@pytest.fixture
中,指定参数params
,就可以利用特殊对象(request
)来引用request.param
。
使用以上带参数的smtp
的测试样例,都会被执行两次。
还有另一种情况,直接对测试function进行参数化。
def add(a, b):
return a + b
@pytest.mark.parametrize("test_input, expected", [
([1, 1], 2),
([2, 2], 4),
([0, 1], 1),
])
def test_add(test_input, expected):
assert expected == add(test_input[0], test_input[1])
利用@pytest.mark.parametrize
,可以无需没有实质意义的fixture,直接得到参数化的效果,测试多组值。
总结 ¶
学会fixture这个利器,pytest才算真正用到家了。 它可以省去很多重复代码,并且自动管理依赖关系。
当然,pytest用到家,不代表Python测试就可以毕业了。 毕竟,有些环境是无法准备的,有些开销是可以避免的。
参考 ¶
以下参考,都是《Full pytest documentation — pytest documentation》的子页面。