Pytest 在哪里存储预期数据

Pytest where to store expected data

测试功能我需要传递参数并查看输出与预期输出匹配。

当函数的响应只是一个可以在测试函数内部定义的小数组或单行字符串时很容易,但是假设我测试的函数修改了一个可能很大的配置文件。或者,如果我明确定义它,结果数组有 4 行长。我应该将它存储在哪里,以便我的测试保持干净且易于维护?

现在,如果那是字符串,我只是将文件放在 .py 测试附近,然后在测试中执行 open()

def test_if_it_works():
    with open('expected_asnwer_from_some_function.txt') as res_file:
        expected_data = res_file.read()
    input_data = ... # Maybe loaded from a file as well
    assert expected_data == if_it_works(input_data)

我看到这种方法有很多问题,比如将这个文件保持最新的问题。它看起来也很糟糕。 我可以让事情变得更好,把它移到固定装置上:

@pytest.fixture
def expected_data()
    with open('expected_asnwer_from_some_function.txt') as res_file:
        expected_data = res_file.read()
    return expected_data

@pytest.fixture
def input_data()
    return '1,2,3,4'

def test_if_it_works(input_data, expected_data):
    assert expected_data == if_it_works(input_data)

这只是将问题转移到另一个地方,通常我需要测试函数在空输入、输入单个项目或多个项目的情况下是否工作,所以我应该创建一个包含所有三种情况或多个的大夹具固定装置。最后代码变得很乱。

如果一个函数需要一个复杂的字典作为输入或返回同样庞大的字典,测试代码就会变得丑陋:

 @pytest.fixture
 def input_data():
     # It's just an example
     return {['one_value': 3, 'one_value': 3, 'one_value': 3,
     'anotherky': 3, 'somedata': 'somestring'], 
      ['login': 3, 'ip_address': 32, 'value': 53, 
      'one_value': 3], ['one_vae': 3, 'password': 13, 'lue': 3]}

用这样的固定装置阅读测试并使它们保持最新是相当困难的。

更新

搜索一段时间后,我找到了一个库,它解决了部分问题,而不是大配置文件,我有大 HTML 响应。这是 betamax.

为了方便使用,我创建了一个夹具:

from betamax import Betamax

@pytest.fixture
def session(request):
    session = requests.Session()
    recorder = Betamax(session)
    recorder.use_cassette(os.path.join(os.path.dirname(__file__), 'fixtures', request.function.__name__)
    recorder.start()
    request.addfinalizer(recorder.stop)
    return session

所以现在在我的测试中我只使用 session fixture 并且我发出的每个请求都会自动序列化到 fixtures/test_name.json 文件所以下次我执行测试而不是做一个真正的HTTP 请求库从文件系统加载它:

def test_if_response_is_ok(session):
   r = session.get("http://google.com")

这非常方便,因为为了使这些固定装置保持最新,我只需要清理 fixtures 文件夹并重新运行我的测试。

想想是否真的需要测试配置文件的全部内容。

如果只需检查几个值或子字符串,请为该配置准备一个预期的模板。测试过的地方会用一些特殊的语法标记为"variables"。然后为模板中的变量准备一个单独的预期值列表。此预期列表可以存储为单独的文件或直接存储在源代码中。

模板示例:

ALLOWED_HOSTS = ['{host}']
DEBUG = {debug}
DEFAULT_FROM_EMAIL = '{email}'

此处,模板变量放在花括号内。

预期值可能如下所示:

host = www.example.com
debug = False
email = webmaster@example.com

甚至作为一个简单的逗号分隔列表:

www.example.com, False, webmaster@example.com

然后您的测试代码可以通过将变量替换为预期值来从模板生成预期的文件。并将预期文件与实际文件进行比较。

单独维护模板和预期值的好处是您可以使用同一模板拥有多个测试数据集。

仅测试变量

一个更好的方法是配置生成方法只为配置文件生成需要的值。这些值可以很容易地通过另一种方法插入到模板中。但优点是测试代码可以直接比较所有的配置变量,比较清晰。

模板

虽然在模板中用需要的值替换变量很容易,但有现成的模板库,只需要一行就可以完成。这里只是几个例子:Django, Jinja, Mako

如果您只有几个测试,那么为什么不将数据作为字符串文字包括在内:

expected_data = """
Your data here...
"""

如果你有一些,或者预期的数据真的很长,我认为你使用 fixtures 是有意义的。

但是,如果您有很多,那么也许采用不同的解决方案会更好。事实上,对于一个项目,我有超过一百个输入和预期输出文件。所以我构建了自己的测试框架(或多或少)。我使用 Nose,但 PyTest 也可以。我创建了一个测试生成器,它遍历了测试文件的目录。对于每个输入文件,都会生成一个测试,将实际输出与预期输出进行比较(PyTest 称之为 parametrizing). Then I documented my framework so others could use it. To review and/or edit the tests, you only edit the input and/or expected output files and never need to look at the python test file. To enable different input files to to have different options defined, I also crated a YAML config file for each directory (JSON would work as well to keep the dependencies down). The YAML data consists of a dictionary where each key is the name of the input file and the value is a dictionary of keywords that will get passed to the function being tested along with the input file. If you're interested, here is the source code and documentation. I recently played with the idea of defining the options as Unittests here(仅需要内置的 unittest 库)但我不确定我是否喜欢它。

我曾经遇到过类似的问题,我必须针对预期文件测试配置文件。我就是这样修复它的:

  1. 在同一位置创建一个与您的测试模块同名的文件夹。将所有预期的文件放入该文件夹。

    test_foo/
        expected_config_1.ini
        expected_config_2.ini
    test_foo.py
    
  2. 创建一个固定装置,负责将此文件夹的内容移动到一个临时文件中。我确实使用了 tmpdir fixture 来解决这个问题。

    from __future__ import unicode_literals
    from distutils import dir_util
    from pytest import fixture
    import os
    
    
    @fixture
    def datadir(tmpdir, request):
        '''
        Fixture responsible for searching a folder with the same name of test
        module and, if available, moving all contents to a temporary directory so
        tests can use them freely.
        '''
        filename = request.module.__file__
        test_dir, _ = os.path.splitext(filename)
    
        if os.path.isdir(test_dir):
            dir_util.copy_tree(test_dir, bytes(tmpdir))
    
        return tmpdir
    

    重要提示: 如果您使用的是 Python 3,请将 dir_util.copy_tree(test_dir, bytes(tmpdir)) 替换为 dir_util.copy_tree(test_dir, str(tmpdir))

  3. 使用你的新灯具。

    def test_foo(datadir):
        expected_config_1 = datadir.join('expected_config_1.ini')
        expected_config_2 = datadir.join('expected_config_2.ini')
    

记住:datadirtmpdir fixture 相同,此外还可以处理放置在与测试模块同名的文件夹中的预期文件。

我相信pytest-datafiles能帮上大忙。不幸的是,它似乎不再被维护了。目前,它运行良好。

这是从文档中摘录的一个简单示例:

import os
import pytest

@pytest.mark.datafiles('/opt/big_files/film1.mp4')
def test_fast_forward(datafiles):
    path = str(datafiles)  # Convert from py.path object to path (str)
    assert len(os.listdir(path)) == 1
    assert os.path.isfile(os.path.join(path, 'film1.mp4'))
    #assert some_operation(os.path.join(path, 'film1.mp4')) == expected_result

    # Using py.path syntax
    assert len(datafiles.listdir()) == 1
    assert (datafiles / 'film1.mp4').check(file=1)