conditional-in-test (SMP803)

SMP803No fixTestsAll rules

Flags if statements, match statements and conditional expressions in test functions.

Why is this bad?

A branching test runs a different check depending on its input or environment, so a passing run does not say which path was verified. With one parametrized case per path, pytest reports each path on its own.

Example

Flagged

tests/test_paths.py

1import sys
2
3
4def test_separator() -> None:
5    if sys.platform == "win32":
6        assert separator() == "\\"
7    else:
8        assert separator() == "/"
Preferred

tests/test_paths.py

 1import sys
 2
 3import pytest
 4
 5
 6@pytest.mark.parametrize(
 7    ("platform", "expected"),
 8    [
 9        pytest.param("win32", "\\", id="windows"),
10        pytest.param("linux", "/", id="linux"),
11    ],
12)
13def test_separator(monkeypatch: pytest.MonkeyPatch, platform: str, expected: str) -> None:
14    monkeypatch.setattr(sys, "platform", platform)
15    assert separator() == expected

What it does

No fix is offered: splitting a test into cases needs knowledge of what each branch tests.

Active on

  • if statements, match statements and conditional expressions in a test function: a test function is a function or method whose name starts with test, in a test module as defined in test-class. Each branch picks which check runs.

    Example: reported

    tests/test_if.py

    1def test_separator() -> None:
    2    if sys.platform == "win32":
    3        assert separator() == "\\"
    4    else:
    5        assert separator() == "/"
    

    tests/test_match.py

    1def test_kind(kind: str) -> None:
    2    match kind:
    3        case "a":
    4            assert parse(kind)
    5        case _:
    6            assert not parse(kind)
    

    tests/test_ternary.py

    1def test_length() -> None:
    2    assert (1 if sys.platform == "win32" else 2) == separator_length()
    

Exceptions

  • Branches inside nested functions, classes and lambdas: they run only when that code is called, not as part of the test’s own path.

    Example: not reported

    tests/test_nested.py

     1def test_callbacks() -> None:
     2    def check(value: int) -> None:
     3        if value:
     4            assert value
     5
     6    class Helper:
     7        def pick(self, value: int) -> int:
     8            return 1 if value else 2
     9
    10    assert (lambda value: 1 if value else 2)(0) == 2
    11    check(1)
    12    assert Helper().pick(0) == 2
    
  • if clauses and conditional items in comprehensions: they shape the items a comprehension builds, rather than pick which check runs.

    Example: not reported

    tests/test_comprehension.py

    1def test_filtered(items: list[int]) -> None:
    2    assert [item for item in items if item] == [1, 2]
    

    tests/test_comprehension_item.py

    1def test_labels(tags: list[str | None]) -> None:
    2    assert [tag if tag is not None else "-" for tag in tags] == ["a", "-"]
    

Other tools