1 Pytest Fixture Mechanism Deep Dive

Reference:

1.1 What Fixtures Are

Fixture 的中文是固定装置的意思. 简单来说, 一个测试包含:

  • Arrange: 准备

  • Act: 执行

  • Assert: 断言 / 验证

  • Clean Up: 清理

在 Python 标准库 unittest 中, setUp 和 tearDown 函数就是用来做准备和清理工作的. 这个叫做 xUnit 风格的 setup / teardown. 在 pytest 中也有 类似的机制和语法. 但这种风格有一个明显的弱点, 就是复用性不够强. 而 Fixture 则是更为强大, 复用性更强, 更灵活的一种机制.

  • fixture 有自己的名字, 这些 fixtures 可以被不同的 函数, 类, 模块 所复用. 而 xUnit 风格的 setup teardown 函数只能在它所起作用的层级发挥作用.

  • fixture 是被模块化的, fixture 可以利用其他的 fixture.

  • fixture 支持更丰富的参数化, 可以被缓存.

  • fixture 逻辑更容易被管理, 你可以按照顺序排列 fixtures.

# -*- coding: utf-8 -*-

"""
Fixture 本质是一个 callable 的函数. 你可以用 ``@pytest.fixture`` decorator
将函数注册为一个 Fixture, 之后你就可以将其像变量一样使用.

Fixture 可以使用其他的 fixture.

Output::

    execute my_fruit()
    execute fruit_basket(my_fruit)
"""

import os
import pytest


class Fruit:
    def __init__(self, name):
        self.name = name

    def __eq__(self, other):
        return self.name == other.name


@pytest.fixture
def my_fruit():
    print("execute my_fruit()")
    return Fruit("apple")


@pytest.fixture
def fruit_basket(my_fruit):
    print("execute fruit_basket(my_fruit)")
    return [Fruit("banana"), my_fruit]


def test_my_fruit_in_basket(my_fruit, fruit_basket):
    assert my_fruit in fruit_basket


if __name__ == "__main__":
    basename = os.path.basename(__file__)
    pytest.main([basename, "-s", "--tb=native"])

1.2 Fixture errors

如果多个 fixtures 之间按照链式彼此依赖, 那么底层的 fixture 出错会导致高层的 fixture 无法被执行, 而用到高层的测试用例也会 fail.

# -*- coding: utf-8 -*-

import os
import pytest


@pytest.fixture
def order():
    return []


@pytest.fixture
def append_first(order):
    print("run append_first")
    # raise ValueError
    order.append(1)


@pytest.fixture
def append_second(order, append_first):
    print("run append_second")
    order.extend([2])


@pytest.fixture(autouse=True)
def append_third(order, append_second):
    print("run append_third")
    order += [3]


def test_order(order):
    assert order == [1, 2, 3]


if __name__ == "__main__":
    basename = os.path.basename(__file__)
    pytest.main([basename, "-s", "--tb=native"])

1.3 “Requesting” Fixture

Fixture 本质上是一个函数, 这个函数只有在你需要的时候才会被使用. 当一个 Fixture 在你的测试用例函数中以参数的形式出现, 那么你的测试用例就 “Request” 了这个 Fixture.

# -*- coding: utf-8 -*-

"""

"""

import os
import pytest


class Fruit:
    def __init__(self, name):
        self.name = name
        self.cubed = False

    def cube(self):
        self.cubed = True


class FruitSalad:
    def __init__(self, *fruit_bowl):
        self.fruit = fruit_bowl
        self._cube_fruit()

    def _cube_fruit(self):
        for fruit in self.fruit:
            fruit.cube()


# Arrange
@pytest.fixture
def fruit_bowl():
    return [Fruit("apple"), Fruit("banana")]


def test_fruit_salad(fruit_bowl):
    # Act
    fruit_salad = FruitSalad(*fruit_bowl)

    # Assert
    assert all(fruit.cubed for fruit in fruit_salad.fruit)


if __name__ == "__main__":
    basename = os.path.basename(__file__)
    pytest.main([basename, "-s", "--tb=native"])

1.4 Fixtures can request other fixtures

Fixture 本身也可以使用其他的 Fixture. 也就是你可以有一些底层的 Fixture, 然后用它们构建更高级的 Fixture, 最后在你的测试用例中使用它们.

1.5 Fixtures are reusable

一个 Fixture 可以被很多个测试用例所使用. 每次使用时候 Fixture 返回的对象都是新的. 举例来说:

Fixture 函数返回的对象如果是 mutable 的, 如果你使用了这个 fixture 很多次并改变了这个对象, 但每次你使用这个 fixture 的时候这个对象都是新的, 并不会出现一个测试用例中的对象被其他测试用例所改变的情况.

# -*- coding: utf-8 -*-

"""
Output::

    execute order()
    execute order()
"""

import os
import pytest


# Arrange
@pytest.fixture
def first_entry() -> str:
    return "a"


# Arrange
@pytest.fixture
def order(first_entry: str) -> list:
    print("execute order()")
    return [first_entry, ]


# below two test reuse same fixture ``order``, which is a mutable object
# but they won't impact each other
def test_string(order: list):
    # Act
    order.append("b")

    # Assert
    assert order == ["a", "b"]


def test_int(order: list):
    # Act
    order.append(2)

    # Assert
    assert order == ["a", 2]


if __name__ == "__main__":
    basename = os.path.basename(__file__)
    pytest.main([basename, "-s", "--tb=native"])

1.6 A test/fixture can request more than one fixture at a time

一个测试用例可以使用多个 Fixture, 就像使用参数一样使用它们. 这些 Fixture 被 evaluate 的顺序和参数定义的顺序一致.

1.7 Fixtures can be requested more than once per test (return values are cached)

一个测试用例可以多次使用同一个 Fixture. 由于一个测试用例的输入参数和 Fixture 的函数名是一致的, 而输入参数又不可能重复, 所以这里说的是一个测试用例用到了两个 fixture, A 和 B, 其中 B 里也用到了 A. 而这时候 A 实际上只被调用了一次, 而 A 的值则是被缓存了起来.

# -*- coding: utf-8 -*-

"""
Output::

    execute order()
    execute first_entry()
    execute append_first()
    append_first = None
    order = ['a']
    first_entry = 'a'
"""

import os
import pytest


# Arrange
@pytest.fixture
def first_entry():
    print("execute first_entry()")
    return "a"


# Arrange
@pytest.fixture
def order():
    print("execute order()")
    return []


# Act
@pytest.fixture
def append_first(order, first_entry):
    print("execute append_first()")
    return order.append(first_entry)


def test_string_only(append_first, order, first_entry):
    # Assert
    assert order == [first_entry]
    print(f"append_first = {append_first!r}")
    print(f"order = {order!r}")
    print(f"first_entry = {first_entry!r}")



if __name__ == "__main__":
    basename = os.path.basename(__file__)
    pytest.main([basename, "-s", "--tb=native"])

1.8 Auto use fixtures

Fixture 可以自动被执行, 而无需被 request. 只需要使用 @pytest.fixture(autouse=True) 语法即可.

# -*- coding: utf-8 -*-


import os
import pytest


@pytest.fixture
def first_entry():
    return "a"


@pytest.fixture
def order(first_entry):
    return []


@pytest.fixture(autouse=True)
def append_first(order, first_entry):
    return order.append(first_entry)


def test_string_only(order, first_entry):
    assert order == [first_entry]


def test_string_and_int(order, first_entry):
    order.append(2)
    assert order == [first_entry, 2]


if __name__ == "__main__":
    basename = os.path.basename(__file__)
    pytest.main([basename, "-s", "--tb=native"])

1.9 Scope

你可以使用一个 Fixture 很多次. 但默认情况下 Fixture 的复用是函数级的, 也就是每个测试用例的函数使用 Fixture 这个 Fixture 都会被重新执行. 有时候你希望改变这个 Fixture 的复用级别, 也就是 Scope, 例如整个类, 或者整个模块中, 这个 Fixture 只被执行一次. 例如只创建一个 Database 连接.

1.10 Dynamic Scope

本质上就是给 Fixture decorator 一个 scope 参数 @pytest.fixture(scope=determine_scope). 这个参数是一个 callable function, 这个 function 需要进行一些运算然后返回 function / class / module / package / session 这 5 个字符串中的一个.

1.11 Setup / Teardown AKA Fixture finalization

Fixture 的本质就是一个函数创建一个可复用的对象或是资源 (比如数据库连接). 有时我们希望在使用后自动清除这个资源.

在 Python 中我们有 with statement. Fixture 也有类似的机制, 先 创建资源, yield 资源, 使用后清除资源.

1.12 Safe teardowns