5

pytest中的fixture

 3 years ago
source link: https://note.qidong.name/2018/01/pytest-fixture/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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版本以后,上面展示的modulefunctionmethod参数,可以去掉。 当然,class的cls不能去掉。 传参的目的,是支持对这些进行调整,然而大多数情况下是无用的,可以省略。 比如,module的可以写成:

def setup_module():
    pass

def teardown_module():
    pass

顾名思义,setup_module就是在同一个module的测试执行前回调一次,teardown_module则是在之后回调。 与之相比,setup_functionteardown_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

其中不乏广泛应用的内容,比如capsystmpdir

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》的子页面。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK