Advanced py.test Fixtures

Floris Bruynooghe

flub@devork.be

Introduction

Fixtures are powerful:

  • Dependency Injection
  • Isolated
  • Composable

Assume you know:

  • py.test
  • basic fixtures

The Basics

You should know this:

import pytest

@pytest.fixture
def foo():
    return 42

def test_foo(foo):
    assert foo == 42

 class TestBar:

    @pytest.fixture
    def bar(self, request):
        def fin():
            print('Teardown of fixture bar')
        request.addfinalizer(fin)
        return 7

    def test_bar(self, foo, bar):
        assert foo != bar

Caching fixtures

Fixture decorator has scope argument:

@pytest.fixture(scope='session')
def foo(request):
    print('session setup')
    def fin():
        print('session finalizer')
    request.addfinalizer(fin)
    return f

@pytest.fixture(scope='function')  # default scope
def bar(request):
    print('funtion setup')
    def fin():
        print('function finalizer')
    request.addfinalizer(fin)
    return b

def test_one(foo, bar):
    pass

def test_two(foo, bar):
    pass

Caching Fixtures

$ py.test test_ex.py -s
========================== test session starts ==========================
platform linux2 -- Python 2.7.6 -- py-1.4.20 -- pytest-2.5.2
plugins: timeout, capturelog, xdist
collected 2 items 

test_ex.py session setup
funtion setup
.function finalizer
function setup
.function finalizer
session finalizer

======================= 2 passed in 0.07 seconds ========================

Process *pytest* finished

Available scopes: function, class, module, session

Interdependent fixtures

Fixture can use fixtures too:

@pytest.fixture(scope='session')
def db_conn():
    return create_db_conn()

@pytest.fixture(scope='module')
def db_table(request, db_conn):
    table = create_table(db_conn, 'foo')
    def fin():
        drop_table(db_conn, table)
    request.addfinalizer(fin)
    return table

def test_bar(db_table):
    pass

Skip/fail in fixture

Fixtures can trigger skipping/failing of all dependent tests:

@pytest.fixture(scope='session')
def redis_client():
    servers = ['localhost', 'venera.clockhouse']
    for hostname in servers:
        try:
            return redis.StrictRedis(hostname)
        except redis.ConnectionError:
            continue
    else:
        pytest.skip('No Redis server found')

Marks

Rember tests can be marked:

@pytest.mark.mymarker
@pytest.mark.other_marker
def test_something():
    pass

Run tests based on markers:

$ py.test -m "not mymarker"

Make them strict:

# pytest.ini
[pytest]
addopts = --strict
markers =
    mymarker: a custom marker

Using Marks from Fixtures

@pytest.fixture
def mongo_client(request):
    marker = request.node.get_marker('mongo_db')
    if not marker:
        db = 'TestDB'
    else:
        def apifun(db):
            return db
        db = apifun(*marker.args, **marker.kwargs)
    return pymongo.MongoClient('127.0.0.1/{}'.format(db))

@pytest.mark.mongo_db('Users')
def test_something(mongo_client):
    pass

Autouse fixtues

Setup/teardown without explicit request:

@pytest.mark.linux
def test_mem_stack():
    assert MemSizes().stack == 42

@pytest.fixture(autouse=True)
def _platform_skip(request):
    marker = request.node.get_marker('linux')
    if marker and platform.system() != 'Linux':
        pytest.skip('N/A on {}'.format(platform.system()))

Parametrising fixtures

  • Individual fixtures can be paremeterised
  • Multiple parameterised fixtures combine
@pytest.fixture(params=['ora', 'pg', 'sqlite'])
def dburi(request):
    return create_db_uri(request.param)

@pytest.fixture(params=['ipv4', 'ipv6'])
def addr_family(request):
    return socket.AF_INET if request.param == 'ipv4' else socket.AF_INET6

def test_txn(dburi):
    inst = MyObj(dburi)
    assert inst.transaction_works()

def test_conn(dburi, addr_family):
    inst = MyObj(dburi, addr_family)
    assert inst.it_works()

Skipping Parameters

Skipping can be done on a parameter level:

try:
    import cx_Oracle as ora
except ImportError:
    ora = None

needs_ora = pytest.mark.skipif(ora is None, reason='No Oracle installed')

@pytest.fixture(params=[
    'pg',
    needs_ora('ora'),
])
def dburi(request):
    return create_db_uri(request.param)

Accessing Fixture Info

Find out what other fixtures are requested:

@pytest.fixture
def db(request):
    if 'transactional_db' in request.fixturenames:
        pytest.fail('Conflicting fixtures')
    return no_transactions_db()

@pytest.fixture
def transactional_db(request):
    if 'db' in request.fixturenames:
        pytest.fail('Conflicting fixtures')
    return transactional_db()

Plugins and Hooks

myproj/
  +- myproj/
  |   +- __init__.py
  |   +- models.py
  +- tests/
      +- contest.py
      +- test_models.py

A few common hooks:

  • pytest_namespace()
  • pytest_addoption(parser)
  • pytest_ignore_collect(path, config)
  • pytest_sessionstart(session)
  • pytest_sessionfinish(session, exitstatus)
  • pytest_assertrepr_compare(config, op, left, right)

See hookspec for full list

Using commandline options

New options an be accessed from fixtures and tests:

#conftest.py
def pytest_addoption(parser):
    parser.addoption('--ci', action='store_true',
                     help='Indicate tests are run on CI server')

@pytest.fixture
def fix(request):
    ci = request.config.getoption('ci')

# Test module
def test_foo(pytestconfig):
    ci = pytestconfig.getoption('ci')

skip-or-fail

Skipping not allowed on CI server:

@pytest.fixture(scope='session')
def redis_client(request):
    servers = ['localhost', 'venera.clockhouse']
    for hostname in servers:
        try:
            return redis.StrictRedis(hostname)
        except redis.ConnectionError:
            continue
    else:
        if request.config.getoption('ci'):
            pytest.fail('No Redis server found')
        else:
            pytest.skip('No Redis server found')

Questions?

Thanks for listening!

flub@devork.be

@flubdevork

abilisoft_small.png