First put a “tough talk”.

If you don’t have fixtures, you better not say you have PyTest.

(Just to reinforce the theme, the bricks can be put down, manual funny)

What is the fixture

Look at the source code

def fixture(
    callable_or_scope=None,
    *args,
    scope="function",
    params=None,
    autouse=False,
    ids=None,
    name=None
) :
    """Decorator to mark a fixture factory function. This decorator can be used, with or without parameters, to define a fixture function. The name of the fixture function can later be referenced to cause its invocation ahead of running tests: test modules or classes can use the ``pytest.mark.usefixtures(fixturename)`` marker. Test functions can directly use fixture names as input arguments in which case the fixture instance returned from the fixture function will be injected.  Fixtures can provide their values to test functions using ``return`` or ``yield`` statements. When using ``yield`` the code block after the ``yield`` statement is executed as teardown code regardless of the test outcome, and must yield exactly once. :arg scope: the scope for which this fixture is shared, one of ``"function"`` (default), ``"class"``, ``"module"``, ``"package"`` or ``"session"`` (``"package"`` is considered **experimental** at this time). This parameter may also be a  callable which receives ``(fixture_name, config)`` as parameters, and must return a ``str`` with one of the values mentioned above. See :ref:`dynamic scope` in the docs for more information. :arg params: an optional list of parameters which will cause multiple invocations of the fixture function and all of the tests using it. The current parameter is available in ``request.param``. :arg autouse: if True, the fixture func is activated for all tests that can see it. If False (the default) then an explicit reference is needed  to activate the fixture. :arg ids: list of string ids each corresponding to the params so that they are part of the test id. If no ids are provided they will be generated automatically from the params. :arg name: the name of the fixture. This defaults to the name of the decorated function. If a fixture is used in the same module in  which it is defined, the function name of the fixture will be shadowed by the function arg that requests the fixture; one way to resolve this is to name the decorated function ``fixture_
      
       `` and then use ``@pytest.fixture(name='
       
        ')``. """
       
      
    if params is not None:
        params = list(params)

    fixture_function, arguments = _parse_fixture_args(
        callable_or_scope,
        *args,
        scope=scope,
        params=params,
        autouse=autouse,
        ids=ids,
        name=name,
    )
    scope = arguments.get("scope")
    params = arguments.get("params")
    autouse = arguments.get("autouse")
    ids = arguments.get("ids")
    name = arguments.get("name")

    if fixture_function and params is None and autouse is False:
        # direct decoration
        return FixtureFunctionMarker(scope, params, autouse, name=name)(
            fixture_function
        )

    return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name)
Copy the code

To summarize

【 definition 】

  • A fixture is a function on which you add annotations@pytest.fixtureTo define the
  • Defined in conftest.py, it can be called without import
  • It is defined in other files and can be called after import
  • Defined in the same file, directly called

[use]

  • The first way to use it is@pytest.mark.usefixtures(fixturename)(If the TestClass modifier works for all methods in the class)
  • The second way to use it is as a function parameter
  • The third use is autouse (no need to display calls, run automatically)

conftest.py

Fixtures are often defined in the confTest.py file.

This is a fixed pyTest file name and cannot be customized.

It must be placed under package, where __init__.py is in the directory.

Fixtures in ConfTest. py can be used in the current directory and its subdirectories without import; PyTest finds them automatically.

Multiple confTest. py files can be created, and the closest one will be used for fixtures with the same name.

Dependency injection

Fixtures implement dependency injection. Dependency injection is a technical form of Inversion of Control.

What are dependency injection and inversion of control

It’s wonderful! Fixtures allow you to add some extra processing without modifying the current function code logic.

An introduction to the sample

# content of ./test_smtpsimple.py
import smtplib

import pytest


@pytest.fixture
def smtp_connection() :
    return smtplib.SMTP("smtp.gmail.com".587, timeout=5)


def test_ehlo(smtp_connection) :
    response, msg = smtp_connection.ehlo()
    assert response == 250
    assert 0  # for demo purposes

Copy the code

Post-execution processing logic

  1. Pytest finds the function starting with test_ and needs a fixture named smtp_Connection
  2. Once found, smtp_connection() is called, returning the SMTP instance
  3. Call test_ehlo(<smtp_connection instance>)smtp_connectionEquals the value of the fixture return

If you want to see what fixtures the file defines, you can use the _ prefix followed by -v

pytest --fixtures test_simplefactory.py
Copy the code

fixture scope & order

Since fixtures can be defined everywhere, wouldn’t it be messy to have more?

Pytest specifies the scope and order in which Fxture can run.

The scope of the fixture is specified by the scope parameter

@pytest.fixture(scope="module")
Copy the code

The default is function. You can choose function, class, Module, package, or session.

Fixtures are created when test is first called, and run and destroy fixtures differently depending on scope

  • Function Each function is run once and destroyed at the end of the function
  • Class Each class is run once and destroyed at the end of the class
  • Module Each module is run once and destroyed at the end of the module
  • Package each package is run once and destroyed at the end of the package
  • Session Each session runs once and is destroyed at the end of the session

Fixtures take precedence in order of magnitude: Session > Package > Module > class > function.

If the scope is the same, the order in which test calls are made, and the dependencies between fixtures.

Autouse’s fixtures take precedence over other fixtures of the same scope.

The sample

import pytest

# fixtures documentation order example
order = []


@pytest.fixture(scope="session")
def s1() :
    order.append("s1")


@pytest.fixture(scope="module")
def m1() :
    order.append("m1")


@pytest.fixture
def f1(f3) :
    order.append("f1")


@pytest.fixture
def f3() :
    order.append("f3")


@pytest.fixture(autouse=True)
def a1() :
    order.append("a1")


@pytest.fixture
def f2() :
    order.append("f2")

def test_order(f1, m1, f2, s1) :
    assert order == ["s1"."m1"."a1"."f3"."f1"."f2"]

Copy the code

Although test_order() is called f1, M1, f2, s1, the results are not in that order

  1. S1 scope for the session
  2. M1 scope for the module
  3. A1 autouse, the default function, precedes session and module, and precedes function and other fixtures
  4. F3 is dependent on F1
  5. F1 test_order() is the first parameter in the list
  6. F2 Third in the test_order() parameter list

Fixture nested

Fixtures decorate functions, so functions also have input parameters.

Fixtures decorate function inputs that can only be other fixtures.

For example, F1 relies on F3, and if F3 is not defined, the execution will report an error in fixture ‘f3’ not found

@pytest.fixture
def f1(f3) :
    order.append("f1")


@pytest.fixture
def f3() :
    order.append("f3")

def test_order(f1) :
    pass
Copy the code

Pass values from test to fixtures

With Request, you can pass values from test to fixtures.

Example 1, smtp_Connection can use smtpServer in module

# content of conftest.py
import smtplib

import pytest


@pytest.fixture(scope="module")
def smtp_connection(request) :
    server = getattr(request.module, "smtpserver"."smtp.gmail.com")
    smtp_connection = smtplib.SMTP(server, 587, timeout=5)
    yield smtp_connection
    print("finalizing {} ({})".format(smtp_connection, server))
    smtp_connection.close()

Copy the code
# content of test_anothersmtp.py
smtpserver = "mail.python.org"  # will be read by smtp fixture


def test_showhelo(smtp_connection) :
    assert 0, smtp_connection.helo()

Copy the code

Example 2, combined with request+mark, passes fixt_data from test_fixt to fixt

import pytest


@pytest.fixture
def fixt(request) :
    marker = request.node.get_closest_marker("fixt_data")
    if marker is None:
        # Handle missing marker in some way...
        data = None
    else:
        data = marker.args[0]
    # Do something with the data
    return data


@pytest.mark.fixt_data(42)
def test_fixt(fixt) :
    assert fixt == 42

Copy the code

fixture setup / teardown

Other test frameworks, UnitTest/Testng, define setup and teardown functions/methods for pre-test initialization and post-test cleanup.

Pytest is compatible with UnitTest and so on.

from loguru import logger


def setup() :
    logger.info("setup")


def teardown() :
    logger.info("teardown")


def test() :
    pass

Copy the code

Fixtures are recommended.

Setup, fixtures define autouse for initialization.

@pytest.fixture(autouse=True)
Copy the code

Autouse’s fixture doesn’t need to be called, but runs itself in the same scope as Test to implement the setup effect.

Instructions for using Autouse

  • Autouse follows the scope rule, scope=”session” the entire session will run only once, and so on
  • Autouse is defined in the module and is used by all functions in the Module (only once if scope=”module”, multiple times if scope=”function”)
  • Autouse is defined in confTest. py and is used by all tests overridden by ConfTest
  • Autouse is defined in the plugin and is used by tests that install the plugin
  • You need to be aware of both scope and definition location when using Autouse

For example, Transact’s default scope is function, which is run automatically before each test function is executed

# content of test_db_transact.py
import pytest


class DB:
    def __init__(self) :
        self.intransaction = []

    def begin(self, name) :
        self.intransaction.append(name)

    def rollback(self) :
        self.intransaction.pop()


@pytest.fixture(scope="module")
def db() :
    return DB()


class TestClass:
    @pytest.fixture(autouse=True)
    def transact(self, request, db) :
        db.begin(request.function.__name__)
        yield
        db.rollback()

    def test_method1(self, db) :
        assert db.intransaction == ["test_method1"]

    def test_method2(self, db) :
        assert db.intransaction == ["test_method2"]

Copy the code

This example can also be implemented using conftest.py instead of Autouse

# content of conftest.py
@pytest.fixture
def transact(request, db) :
    db.begin()
    yield
    db.rollback()
Copy the code
@pytest.mark.usefixtures("transact")
class TestClass:
    def test_method1(self) :.Copy the code

Teardown, you can use the yield keyword in fixtures to clean things up.

Example, scope is module, and at the end of module, yield is followed by print() and smtp_connection.close()

# content of conftest.py
import smtplib

import pytest


@pytest.fixture(scope="module")
def smtp_connection() :
    smtp_connection = smtplib.SMTP("smtp.gmail.com".587, timeout=5)
    yield smtp_connection  # provide the fixture value
    print("teardown smtp")
    smtp_connection.close()

Copy the code

This can be further simplified by using the with keyword, which automatically cleans up the context by executing smtp_connection.close()

# content of test_yield2.py
import smtplib

import pytest


@pytest.fixture(scope="module")
def smtp_connection() :
    with smtplib.SMTP("smtp.gmail.com".587, timeout=5) as smtp_connection:
        yield smtp_connection  # provide the fixture value

Copy the code

Fixture parameterized

Please forgive me if I skip pyTest parameterization.

Because I think if you want to parameterize pyTest, you have to go to parameterized articles first, not fixtures.

Put this section in parameterization to make it easier to retrieve later.

A brief review of

I started this article with a source code introduction to what fixtures are and a brief summary of their definition and usage. I then explained dependency injection to better understand how fixture technology works. The getting started example shows examples from the official website to expand on scope, ordering, nesting, passing values, and initialization and cleanup.

If you have any problems, welcome to discuss them.

For more practical content, check out the follow-up chapter tep Best Practices.

The resources

En.wikipedia.org/wiki/Depend…

En.wikipedia.org/wiki/Invers…

Docs.pytest.org/en/stable/c…

Copyright notice: this article is the blogger’s original article, please retain the original link and author.

If you like my article, please pay attention to the public number to support, thank you ha ha ha.