Pytest-PROPOSAL: Parametrize with fixtures

::: {.warning}
::: {.title}
Warning
:::

This document outlines a proposal around using fixtures as input of
parametrized tests or fixtures.
:::

Problem

As a user I have functional tests that I would like to run against
various scenarios.

In this particular example we want to generate a new project based on a
cookiecutter template. We want to test default values but also data that
emulates user input.

  • use default values
  • emulate user input
    • specify 'author'
    • specify 'project_slug'
    • specify 'author' and 'project_slug'

This is how a functional test could look like:

import pytest


@pytest.fixture
def default_context():
    return {"extra_context": {}}


@pytest.fixture(
    params=[
        {"author": "alice"},
        {"project_slug": "helloworld"},
        {"author": "bob", "project_slug": "foobar"},
    ]
)
def extra_context(request):
    return {"extra_context": request.param}


@pytest.fixture(params=["default", "extra"])
def context(request):
        return request.getfuncargvalue("default_context")
    else:
        return request.getfuncargvalue("extra_context")


def test_generate_project(cookies, context):
    """Call the cookiecutter API to generate a new project from a
    template.
    """
    result = cookies.bake(extra_context=context)

    assert result.exception is None
    assert result.project.isdir()

Issues

  • By using request.getfuncargvalue() we rely on actual fixture
    function execution to know what fixtures are involved, due to its
    dynamic nature
  • More importantly, request.getfuncargvalue() cannot be combined
    with parametrized fixtures, such as extra_context
  • This is very inconvenient if you wish to extend an existing test
    suite by certain parameters for fixtures that are already used by
    tests

pytest version 3.0 reports an error if you try to run above code:

Failed: The requested fixture has no parameter defined for the current
test.

Requested fixture 'extra_context'

Proposed solution

A new function that can be used in modules can be used to dynamically
define fixtures from existing ones.

pytest.define_combined_fixture(
    name="context", fixtures=["default_context", "extra_context"]
)

The new fixture context inherits the scope from the used fixtures and
yield the following values.

  • {}
  • {'author': 'alice'}
  • {'project_slug': 'helloworld'}
  • {'author': 'bob', 'project_slug': 'foobar'}

Alternative approach

A new helper function named fixture_request would tell pytest to yield
all parameters marked as a fixture.

::: {.note}
::: {.title}
Note
:::

The pytest-lazy-fixture
plugin implements a very similar solution to the proposal below, make
sure to check it out.
:::

@pytest.fixture(
    params=[
        pytest.fixture_request("default_context"),
        pytest.fixture_request("extra_context"),
    ]
)
def context(request):
    """Returns all values for ``default_context``, one-by-one before it
    does the same for ``extra_context``.

    request.param:
        - {}
        - {'author': 'alice'}
        - {'project_slug': 'helloworld'}
        - {'author': 'bob', 'project_slug': 'foobar'}
    """
    return request.param

The same helper can be used in combination with
pytest.mark.parametrize.

@pytest.mark.parametrize(
    "context, expected_response_code",
    [
        (pytest.fixture_request("default_context"), 0),
        (pytest.fixture_request("extra_context"), 0),
    ],
)
def test_generate_project(cookies, context, exit_code):
    """Call the cookiecutter API to generate a new project from a
    template.
    """
    result = cookies.bake(extra_context=context)