【问题标题】:Pytest where to store expected dataPytest 在哪里存储预期数据
【发布时间】:2015-04-14 12:28:02
【问题描述】:

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

当函数的响应只是一个可以在测试函数内部定义的小数组或单行字符串时,这很容易,但是假设我测试的函数修改了一个可能很大的配置文件。或者,如果我明确定义它,结果数组是 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 固定装置,我发出的每个请求都会自动序列化到fixtures/test_name.json 文件,所以下次我执行测试而不是执行真正的 HTTP 请求库时,会从文件系统:

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

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

【问题讨论】:

  • 从开发者的角度来看,有TC故障,需要调试。他有一个测试用例的名字,所以他去这个名字的功能。如果你把预期的数据分开保存,你就会强迫他来回跳来比较它。只要您可以将测试输入和预期输入保留在测试用例中或尽可能靠近它,恕我直言。
  • @m.wasowski 那么您的建议是什么?将大块数据保留在测试函数的主体中?例如,如果它是二进制的怎么办?..
  • 无论如何,二进制读起来不太好,所以情况不同。我只是说,在测试中保持数据就近使用至少与 DRY 一样重要。这一切都取决于具体情况。当然我建议你缩进你的数据......你写的这个return真的很难看,很难理解。

标签: python pytest


【解决方案1】:

我曾经遇到过类似的问题,我必须根据预期文件测试配置文件。我就是这样解决的:

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

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

    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 夹具相同,此外还可以处理放置在具有测试模块名称的文件夹中的预期文件。

【讨论】:

  • 这有一个额外的好处是测试不会相互干扰,因为每个测试都会有一个数据文件的副本。
  • 没错。这样您就可以使用多个内核运行测试,而不必担心资源的并发性(在这种情况下是预期的文件)。
  • 是的,我可能会这样做,但我认为这可能太笨拙了。顺便说一句,我没有得到资源并发的东西。文件是只读的,会出什么问题?
  • 你是对的。对于只读文件,除非这些文件由于某种原因被锁定,否则一切都应该正常工作。创建或更改多个测试使用的文件时,并发问题会经常发生。
  • 在 python 3 上 dir_util.copy_tree(test_dir, str(tmpdir)) 需要进行这项工作。
【解决方案2】:

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

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

如果您有少量数据,或者预期数据非常长,我认为您使用固定装置是有道理的。

但是,如果您有很多,那么不同的解决方案可能会更好。事实上,对于一个项目,我有超过一百个输入和预期输出文件。所以我建立了自己的测试框架(或多或少)。我使用了鼻子,但 PyTest 也可以。我创建了一个测试生成器,它遍历测试文件的目录。对于每个输入文件,都会生成一个测试,将实际输出与预期输出进行比较(PyTest 称之为parametrizing)。然后我记录了我的框架,以便其他人可以使用它。要查看和/或编辑测试,您只需编辑输入和/或预期输出文件,无需查看 python 测试文件。为了使不同的输入文件能够定义不同的选项,我还为每个目录创建了一个 YAML 配置文件(JSON 也可以用来降低依赖关系)。 YAML 数据由一个字典组成,其中每个键是输入文件的名称,值是一个关键字字典,这些关键字将与输入文件一起传递给正在测试的函数。如果你有兴趣,这里是source codedocumentation。我最近尝试将选项定义为 Unittests here(只需要内置的 unittest 库),但我不确定我是否喜欢它。

【讨论】:

  • 好吧,当我使用字符串文字时,我几乎总是遇到正确格式的问题。如果来自subprocess 的某些命令返回我想使用的结果,它将包含特殊符号,当我将其复制粘贴为字符串文字时,这些特殊符号就会消失。我会看看你的代码。谢谢你。不幸的是,这看起来像是轮子改造。就像以前没有人遇到过同样的问题。
  • 你指的是什么特殊字符?外部文件不也存在这些差异吗?不知道这是一个怎样的因素。
  • 通常它是制表符而不是空格的混合。因此,当将控制台工具的输出复制到字符串文字然后进行比较时,它们会有所不同。我尝试将\t 添加到来回的字符串中,最后只是序列化了输出字符串。如果数据是二进制类型或具有奇怪的编码,这也可能很糟糕。
【解决方案3】:

我相信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)

【讨论】:

  • 来自链接的 PyPi 条目:关于维护的注意事项:此项目已维护,错误报告或拉取请求将得到解决。几乎没有什么活动,因为它很简单,不需要任何更改。
【解决方案4】:

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

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

模板示例:

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

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

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

仅测试变量

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

模板

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

【讨论】:

  • 我想到了所有这些想法。如果我将一小部分配置作为固定装置,我并没有完全测试该功能,因为配置中未包含的部分可能会破坏我的功能。例如,如果我有重复的模板标签,那么测试将通过,但使用真正的配置我会失败,因为它只会替换标签的第一次出现。是的,Jinja 很好,但问题是 dovecot 或 exim 等配置文件使用保留的 Jinja 关键字,因此通常会失败并显示“语法无效”。
  • 您可以考虑将您的生成方法分成两部分:仅生成配置值并从值和模板生成整个配置。配置值可以作为字典或列表返回。在重构您的方法之前,首先编写一个简单比较完整预期配置文件的测试。使用原始方法的大部分,编写两个用于值生成和模板渲染的新方法。单元测试单个产生的值。然后用对两个新方法的调用替换旧方法。所有测试仍必须通过。
  • 最后我会得到未经测试的 combine_chunks 函数,或者如果我尝试测试它,我会遇到像现在这样的问题 - 在哪里存储最终配置,如何加载它等。如果函数执行正则表达式搜索文件中的一行然后附加一些内容,我不知道如何将生成分成两部分。
猜你喜欢
  • 2014-10-28
  • 1970-01-01
  • 2015-11-17
  • 2012-04-11
  • 2011-12-14
  • 2020-01-07
  • 2011-12-09
  • 2019-05-24
  • 2016-07-18
相关资源
最近更新 更多