Skip to content

ryangalamb/pytest-funparam

Repository files navigation

pytest-funparam

pytest-funparam makes it easy to write parametrized tests.

Unlike pytest.mark.parametrize, pytest-funparam:

  • includes the failing parameter in pytest tracebacks;
  • enables static type checking of parameters; and
  • keeps parameters and assertions closer together.

You can install "pytest-funparam" via pip from PyPI:

$ pip install pytest-funparam

Inside a test function, decorate a function with the funparam fixture:

def test_addition(funparam):
    @funparam
    def verify_sum(a, b, expected):
        assert a + b == expected

    verify_sum(1, 2, 3)
    verify_sum(2, 2, 5)  # OOPS!
    verify_sum(4, 2, 6)

And run pytest:

$ pytest
============================= test session starts ==============================
collected 3 items

test_readme.py .F.                                                       [100%]

=================================== FAILURES ===================================
_______________________________ test_addition[1] _______________________________

    def test_addition(funparam):
        @funparam
        def verify_sum(a, b, expected):
            assert a + b == expected

        verify_sum(1, 2, 3)
>       verify_sum(2, 2, 5)  # OOPS!

test_readme.py:7:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

a = 2, b = 2, expected = 5

    @funparam
    def verify_sum(a, b, expected):
>       assert a + b == expected
E       assert (2 + 2) == 5

test_readme.py:4: AssertionError
========================= 1 failed, 2 passed in 0.03s ==========================

The test_addition test case was split into 3 tests, one for each verify_sum call.

Because funparam is parametrizing the test calls, it even works with commands like pytest --last-failed:

$ pytest --last-failed
============================= test session starts ==============================
collected 1 item

test_readme.py F                                                         [100%]

=================================== FAILURES ===================================
_______________________________ test_addition[1] _______________________________

    def test_addition(funparam):
        @funparam
        def verify_sum(a, b, expected):
            assert a + b == expected

        verify_sum(1, 2, 3)
>       verify_sum(2, 2, 5)  # OOPS!

test_readme.py:7:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

a = 2, b = 2, expected = 5

    @funparam
    def verify_sum(a, b, expected):
>       assert a + b == expected
E       assert (2 + 2) == 5

test_readme.py:4: AssertionError
============================== 1 failed in 0.01s ===============================

Mark tests by using the .marks() method of your funparam function.

import pytest

def test_addition(funparam):
    @funparam
    def verify_sum(a, b, expected):
        assert a + b == expected

    verify_sum(1, 2, 3)
    verify_sum.marks(pytest.mark.skip)(2, 2, 5)
    verify_sum(4, 2, 6)
$ pytest
============================= test session starts ==============================
collected 3 items

test_readme.py .s.                                                       [100%]

========================= 2 passed, 1 skipped in 0.01s =========================

Similarly, add an id to a test using the .id() method of your funparam function:

def test_addition(funparam):
    @funparam
    def verify_sum(a, b, expected):
        assert a + b == expected

    verify_sum.id("one and two")(1, 2, 3)
    verify_sum.id("two and two")(2, 2, 5)
    verify_sum.id("four and two")(4, 2, 6)
$ pytest --collect-only
============================= test session starts ==============================
collected 3 items

<Module test_readme.py>
  <Function test_addition[one and two]>
  <Function test_addition[two and two]>
  <Function test_addition[four and two]>

========================== 3 tests collected in 0.01s ==========================

You can also use the shorthand for assigning an id. (It does the same thing as calling .id().)

def test_addition(funparam):
    @funparam
    def verify_sum(a, b, expected):
        assert a + b == expected

    verify_sum["one and two"](1, 2, 3)
    verify_sum["two and two"](2, 2, 5)
    verify_sum["four and two"](4, 2, 6)
$ pytest --collect-only
============================= test session starts ==============================
collected 3 items

<Module test_readme.py>
  <Function test_addition[one and two]>
  <Function test_addition[two and two]>
  <Function test_addition[four and two]>

========================== 3 tests collected in 0.01s ==========================

pytest-funparam has full type annotations. The funparam fixture returns a FunparamFixture object. You can import it from pytest_funparam:

import pytest
from pytest_funparam import FunparamFixture

def test_addition(funparam: FunparamFixture):

    @funparam
    def verify_sum(a: int, b: int , expected: int):
        assert a + b == expected

    # These are valid
    verify_sum(1, 2, 3)
    verify_sum['it accommodates ids'](2, 2, 4)
    # Marks work too!
    verify_sum.marks(pytest.mark.xfail)(2, 2, 9)

    # This will be marked as invalid (since it's not an int)
    verify_sum(1, '2', 3)

    # Using id/marks will still preserve the function's typing.
    verify_sum['should be an int'](1, 2, '3')
$ mypy
test_readme.py:17: error: Argument 2 to "verify_sum" has incompatible type "str"; expected "int"
test_readme.py:20: error: Argument 3 to "verify_sum" has incompatible type "str"; expected "int"
Found 2 errors in 1 file (checked 1 source file)

Distributed under the terms of the MIT license, "pytest-funparam" is free and open source software

If you encounter any problems, please file an issue along with a detailed description.