pytest:对同一接口的不同实现的可重用测试

时间:2014-10-08 21:08:42

标签: python unit-testing pytest

想象一下,我在模块Bar中实现了一个名为foo的实用程序(可能是一个类),并为其编写了以下测试。

test_foo.py:

from foo import Bar as Implementation
from pytest import mark

@mark.parametrize(<args>, <test data set 1>)
def test_one(<args>):
    <do something with Implementation and args>

@mark.parametrize(<args>, <test data set 2>)
def test_two(<args>):
    <do something else with Implementation and args>

<more such tests>

现在想象一下,在未来我希望能够编写相同接口的不同实现。我希望这些实现能够重用为上述测试套件编写的测试:唯一需要改变的是

  1. 导入Implementation
  2. <test data set 1><test data set 2>等。
  3. 所以我正在寻找一种以可重用的方式编写上述测试的方法,这将允许接口的新实现的作者能够通过将实现和测试数据注入其中来使用测试,而无需修改包含测试原始规范的文件。

    在pytest中做这件事的好方法是什么?

    =============================================== =====================

    =============================================== =====================

    这是一个单元测试版本(不是很漂亮但可以)。

    define_tests.py:

    # Single, reusable definition of tests for the interface. Authors of
    # new implementations of the interface merely have to provide the test
    # data, as class attributes of a class which inherits
    # unittest.TestCase AND this class.
    class TheTests():
    
        def test_foo(self):
            # Faking pytest.mark.parametrize by looping
            for args, in_, out in self.test_foo_data:
                self.assertEqual(self.Implementation(*args).foo(in_),
                                 out)
    
        def test_bar(self):
            # Faking pytest.mark.parametrize by looping
            for args, in_, out in self.test_bar_data:
                self.assertEqual(self.Implementation(*args).bar(in_),
                                 out)
    

    v1.py:

    # One implementation of the interface
    class Implementation:
    
        def __init__(self, a,b):
            self.n = a+b
    
        def foo(self, n):
            return self.n + n
    
        def bar(self, n):
            return self.n - n
    

    v1_test.py:

    # Test for one implementation of the interface
    from v1 import Implementation
    from define_tests import TheTests
    from unittest import TestCase
    
    # Hook into testing framework by inheriting unittest.TestCase and reuse
    # the tests which *each and every* implementation of the interface must
    # pass, by inheritance from define_tests.TheTests
    class FooTests(TestCase, TheTests):
    
        Implementation = Implementation
    
        test_foo_data = (((1,2), 3,  6),
                         ((4,5), 6, 15))
    
        test_bar_data = (((1,2), 3,  0),
                         ((4,5), 6,  3))
    

    任何人(甚至是该库的客户端)编写此接口的另一个实现

    • 可以重用define_tests.py
    • 中定义的一组测试
    • 将自己的测试数据注入测试
    • 不修改任何原始文件

4 个答案:

答案 0 :(得分:5)

这是parametrized test fixtures的一个很好的用例。

您的代码可能如下所示:

from foo import Bar, Baz

@pytest.fixture(params=[Bar, Baz])
def Implementation(request):
    return request.param

def test_one(Implementation):
    assert Implementation().frobnicate()

这将test_one运行两次:一次是Implementation = Bar,一次是Implementation = Baz。

请注意,由于实现只是一个工具,您可以更改其范围,或进行更多设置(可能实例化该类,可能以某种方式配置它)。

如果与pytest.mark.parametrize装饰器一起使用,pytest将生成所有排列。例如,假设上面的代码,这个代码在这里:

@pytest.mark.parametrize('thing', [1, 2])
def test_two(Implementation, thing):
    assert Implementation(thing).foo == thing

test_two将运行四次,具有以下配置:

  • 实施= Bar,thing = 1
  • 实施= Bar,thing = 2
  • 实施= Baz,thing = 1
  • 实施= Baz,thing = 2

答案 1 :(得分:0)

没有类继承就不能这样做,但是你不必使用unittest.TestCase。为了使它更加pytest你可以使用灯具。

它允许您进行夹具参数化,或使用其他修复。

我尝试创建一个简单的例子。

class SomeTest:

    @pytest.fixture
    def implementation(self):
        return "A"

    def test_a(self, implementation):
        assert "A" == implementation


class OtherTest(SomeTest):

   @pytest.fixture(params=["B", "C"])
   def implementation(self, request):
       return request.param


def test_a(self, implementation):
    """ the "implementation" fixture is not accessible out of class """ 
    assert "A" == implementation

并且第二次测试失败

    def test_a(self, implementation):
>       assert "A" == implementation
E       assert 'A' == 'B'
E         - A
E         + B

    def test_a(self, implementation):
>       assert "A" == implementation
E       assert 'A' == 'C'
E         - A
E         + C

  def test_a(implementation):
        fixture 'implementation' not found

不要忘记你必须在pytest.ini中定义python_class = *Test

答案 2 :(得分:0)

基于条件插件的解决方案

实际上有一种技术可以依靠pytest_plugins列表,您可以在该列表上以超越pytest的条件(即环境变量和命令行参数)为条件。请考虑以下内容:

if os.environ["pytest_env"] == "env_a":
    pytest_plugins = [
        "projX.plugins.env_a",
    ]
elif os.environ["pytest_env"] == "env_b":
    pytest_plugins = [
        "projX.plugins.env_b",
    ]

我创建了一个GitHub存储库,以共享一些pytest实验,这些实验演示了上述技术,并附带了注释和测试运行结果。此特定问题的相关部分是conditional_plugins实验。 https://github.com/jxramos/pytest_behavior

这将使您可以使用同一个测试模块以及同名夹具的两个不同实现。但是,您需要为每个实现调用一次测试,并使用选择机制挑选出感兴趣的夹具实现。因此,您需要两个pytest会话来完成对两个夹具变体的测试。

为了重用已经存在的测试,需要建立一个比要重用的项目更高的根目录,并在其中定义一个conftest.py文件来选择插件。这可能还不够,因为如果您保留目录结构不变,那么测试模块和任何中间conftest文件的压倒性行为。但是,如果您可以自由地重新排列文件并使其保持不变,则只需从测试模块到根目录的路径行之外获取现有的conftest文件,然后重命名该文件即可将其检测为插件

插件的配置/命令行选择

Pytest实际上具有一个-p命令行选项,您可以在其中依次列出多个插件以指定插件文件。您可以通过在pytest_behavior存储库中的ini_plugin_selection实验中了解更多有关该控件的信息。

对灯具值进行参数化

在撰写本文时,这是pytest核心功能的一项正在进行中的工作,但是有一个第三方插件pytest-cases支持一种概念,其中夹具本身可以用作测试用例的参数。借助该功能,您可以为同一测试用例对多个夹具进行参数化,其中每个夹具都由每个API实施支持。这听起来像是您的用例的理想解决方案,但是您仍然需要用新的源装饰现有的测试模块,以允许对您可能不允许的灯具进行这种参数化。

在一个公开的pytest问题#349 Using fixtures in pytest.mark.parametrize中,特别是在此comment中,您将看到丰富的讨论。他链接到他所写的具体example,它演示了新的夹具参数化语法。

评论

我认为,测试夹具层次结构可以一直构建在测试模块之上,直至执行的根目录,这更倾向于 fixture 重用,但没有那么多 test模块重用。如果您考虑一下,您可以在一个公用子文件夹中向上编写多个固定装置,在该子文件夹中,一堆测试模块可能会潜入许多子目录中。这些测试模块中的每个模块都可以访问在该父conftest.py中定义的灯具,但是如果不做额外的工作,即使在所有中间conftest.py文件中重复使用相同的名称,它们也只能为每个灯具在每个中间short temp = ; result[0] = (byte) ((temp >>> 8) & 0xFF); result[1] = (byte)((temp) & 0xFF); 文件中获得一个定义。该层次结构。通过pytest夹具覆盖机制选择了最接近测试模块的夹具,但是解析在测试模块处停止,并且不会越过夹具到达测试模块下方的任何文件夹中,在这些文件夹中可能会发现变化。本质上,从测试模块到根目录只有一条路径,它将夹具定义限制为一个。这使我们可以固定许多测试模块的关系。

答案 3 :(得分:0)

我做了一些类似于 @Daniel Barto 所说的,添加了额外的装置。

假设您有 1 个接口和 2 个实现:

class Imp1(InterfaceA):
    pass # Some implementation.
class Imp2(InterfaceA):
    pass # Some implementation.

您确实可以将测试封装在子类中:

@pytest.fixture
def imp_1():
    yield Imp1()

@pytest.fixture
def imp_2():
    yield Imp2()


class InterfaceToBeTested:
    @pytest.fixture
    def imp(self):
        pass
    
    def test_x(self, imp):
        assert imp.test_x()
    
    def test_y(self, imp):
        assert imp.test_y()

class TestImp1(InterfaceToBeTested):
    @pytest.fixture
    def imp(self, imp_1):
        yield imp_1

    def test_1(self, imp):
        assert imp.test_1()

class TestImp2(InterfaceToBeTested):
    @pytest.fixture
    def imp(self, imp_2):
        yield imp_2

注意:请注意如何通过添加额外的派生类并覆盖返回实现的夹具,您可以对其运行所有测试,并且如果存在特定于实现的测试,它们也可以写在那里。