pytest 中的分组测试:类与普通函数

问题描述 投票:0回答:4

我正在使用 pytest 来测试我的应用程序。 pytest 支持两种(据我所知)编写测试的方法:

  1. 上课时:

test_feature.py -> 类 TestFeature -> def test_feature_sanity

  1. 在函数中:

test_feature.py -> def test_feature_sanity

是否需要采用班级分组测试的方式?是否允许向后移植 unittest 内置模块? 您认为哪种方法更好?为什么?

python pytest
4个回答
120
投票

这个答案为 pytest 中的 TestClass 提供了两个引人注目的用例:

  • 属于给定类的多个测试方法的联合参数化。
  • 通过子类继承重用测试数据和测试逻辑

属于给定类的多个测试方法的联合参数化。

pytest 参数化装饰器

@pytest.mark.parametrize
可用于使输入可用于类中的多个方法。在下面的代码中,输入
param1
param2
可用于每种方法
TestGroup.test_one
TestGroup.test_two

# in file `test_class_parametrization.py`
import pytest

@pytest.mark.parametrize(
    ("param1", "param2"),
    [
        ("a", "b"),
        ("c", "d"),
    ],
)
class TestGroup:
    """A class with common parameters, `param1` and `param2`."""

    @pytest.fixture
    def fixt(self) -> int:
        """This fixture will only be available within the scope of TestGroup"""
        return 123

    def test_one(self, param1: str, param2: str, fixt: int) -> None:
        print("\ntest_one", param1, param2, fixt)

    def test_two(self, param1: str, param2: str) -> None:
        print("\ntest_two", param1, param2)
$ pytest -s test_class_parametrization.py
================================================================== test session starts ==================================================================
platform linux -- Python 3.8.6, pytest-6.2.1, py-1.10.0, pluggy-0.13.1
rootdir: /home/jbss
plugins: pylint-0.18.0
collected 4 items

test_class_parametrization.py
test_one a b 123
.
test_one c d 123
.
test_two a b
.
test_two c d
.

=================================================================== 4 passed in 0.01s ===================================================================

通过子类继承重用测试数据和测试逻辑

我将使用从另一个答案中获取的代码的修改版本来演示从

TestClass
继承类属性/方法到
TestSubclass
的有用性:

# in file `test_inheritance.py`
class TestClass:
    VAR: int = 3
    DATA: int = 4

    def test_var_positive(self) -> None:
        assert self.VAR >= 0


class TestSubclass(TestClass):
    VAR: int = 8

    def test_var_even(self) -> None:
        assert self.VAR % 2 == 0

    def test_data(self) -> None:
        assert self.DATA == 4

在此文件上运行

pytest
会导致运行 four 测试:

$ pytest -v test_inheritance.py
=========== test session starts ===========
platform linux -- Python 3.8.2, pytest-5.4.2, py-1.8.1
collected 4 items

test_inheritance.py::TestClass::test_var_positive PASSED
test_inheritance.py::TestSubclass::test_var_positive PASSED
test_inheritance.py::TestSubclass::test_var_even PASSED
test_inheritance.py::TestSubclass::test_data PASSED

在子类中,继承的

test_var_positive
方法使用更新后的值
self.VAR == 8
运行,新定义的
test_data
方法针对继承的属性
self.DATA == 4
运行。这种方法和属性继承提供了一种灵活的方式来重用或修改不同测试用例组之间的共享功能。


88
投票

关于将测试组织到模块和类中没有严格的规则。这是个人喜好的问题。最初,我尝试将测试组织到班级中,一段时间后,我意识到我没有其他级别的组织的用处。现在我只是将测试函数收集到模块(文件)中。

我可以看到一个有效的用例,即某些测试可以逻辑地组织到同一个文件中,但仍然具有额外的类组织级别(例如使用类范围的固定装置)。但这也可以通过拆分成多个模块来完成。


40
投票

通常在单元测试中,我们测试的对象是单个函数。也就是说,单个函数会引起多个测试。在阅读测试代码时,将单个单元的测试以某种方式分组在一起很有用(这也允许我们运行特定功能的所有测试),因此这给我们留下了两个选择:

  1. 将每个功能的所有测试放在专用模块中
  2. 将每个函数的所有测试放在一个类中

在第一种方法中,我们仍然有兴趣以某种方式对与源模块相关的所有测试(例如

utils.py
)进行分组。现在,由于我们已经使用模块对function的测试进行分组,这意味着我们应该使用package对源模块的测试进行分组。

结果是一个源 function 映射到一个测试 module,一个源 module 映射到一个测试 package

在第二种方法中,我们将一个源函数映射到一个测试类(例如

my_function()
->
TestMyFunction
),并将一个源模块映射到一个测试模块(例如
utils.py
->
test_utils.py
) .

这也许取决于具体情况,但第二种方法,即针对您正在测试的每个功能进行一类测试,对我来说似乎更清楚。此外,如果我们正在测试源/方法,那么我们可以简单地使用测试类的继承层次结构,并且仍然保留一个源模块 -> 一个测试模块映射。

最后,与包含多个函数测试的平面文件相比,这两种方法的另一个好处是,通过类/模块已经识别正在测试哪个函数,您可以为实际测试提供更好的名称,例如

test_does_x
test_handles_y
而不是
test_my_function_does_x
test_my_function_handles_y


0
投票

还有一个 pytest 插件,用于使用 Rspec (Ruby on Rails) 和 Jasmine (Javascript) 中使用的类似描述的样式来组织子功能中的测试。

https://github.com/pytest-dev/pytest-describe

def describe_my_function():
    def with_default_arguments():
    def with_some_other_arguments():
    def it_throws_exception():
    def it_handles_exception():

def describe_some_other_function():
    def it_returns_true():
    def it_returns_false():

用例示例:

import pytest

class Wallet:

    def __init__(self, initial_amount=0):
        self.balance = initial_amount

    def spend_cash(self, amount):
        if self.balance < amount:
            raise ValueError(f'Not enough available to spend {amount}')
        self.balance -= amount

    def add_cash(self, amount):
        self.balance += amount
        
        
def describe_wallet():
    
    def describe_start_empty():
        
        @pytest.fixture
        def wallet():
            return Wallet()

        def initial_amount(wallet):
            assert wallet.balance == 0
    
        def add_cash(wallet):
            wallet.add_cash(80)
            assert wallet.balance == 80

        def spend_cash(wallet):
            with pytest.raises(ValueError):
                wallet.spend_cash(10)
© www.soinside.com 2019 - 2024. All rights reserved.