Decorator Placement & Compatibility
When using @pytest.mark.repeated alongside other pytest markers and decorators, placement and ordering matter.
General Rule: Place @pytest.mark.repeated Last (Bottom)
Recommended: Put @pytest.mark.repeated as the last decorator (closest to the function definition):
import pytest
@pytest.mark.slow
@pytest.mark.integration
@pytest.mark.repeated(times=50, threshold=48)
def test_integration_with_external_api():
"""repeated marker is last (bottom) - RECOMMENDED"""
response = call_external_api()
assert response.status_code == 200
Why Bottom Placement?
Decorators in Python are applied bottom-to-top. The decorator closest to the function executes first in the wrapper chain:
@decorator_A
@decorator_B
@decorator_C
def my_function():
pass
# Equivalent to:
my_function = decorator_A(decorator_B(decorator_C(my_function)))
Placing @pytest.mark.repeated at the bottom ensures:
- Other markers are applied first to the base test function
- Repeated wrapper is outermost, controlling the overall execution
- Each repetition sees the full decorated test, not a partial one
Common Scenarios
With @pytest.mark.parametrize
@pytest.mark.parametrize("input_val,expected", [(1, 2), (2, 4), (3, 6)])
@pytest.mark.repeated(times=10, threshold=9)
def test_doubling(input_val, expected):
"""
Each parameter combination is repeated 10 times.
Result: 3 parameter sets × 10 repetitions = 30 total test runs
"""
result = random_doubler(input_val) # Occasionally returns wrong value
assert result == expected
Order matters:
- ✅ parametrize → repeated: Each parameter combo repeated 10 times
- ❌ repeated → parametrize: Would repeat the entire parametrized test 10 times (probably not what you want)
With @pytest.fixture (as argument)
@pytest.mark.repeated(times=20, threshold=19)
def test_with_fixture(temp_database):
"""
Fixture runs once per repetition.
repeated marker at bottom ensures fixture is properly set up each time.
"""
result = query_random_record(temp_database)
assert result is not None
Fixtures are function arguments, not decorators, so they're always inside the repetition loop.
With @pytest.mark.skip or @pytest.mark.skipif
@pytest.mark.skipif(sys.platform == "win32", reason="Unix only")
@pytest.mark.repeated(times=50, threshold=48)
def test_unix_specific_feature():
"""
Skip condition evaluated before repetition.
If test is skipped, it's skipped entirely (no repetitions).
"""
result = unix_only_function()
assert result > 0
Skip markers should typically be above (before) repeated so the skip is evaluated first.
With @pytest.mark.xfail
@pytest.mark.xfail(reason="Known flaky external dependency")
@pytest.mark.repeated(times=100, threshold=95)
def test_flaky_external_service():
"""
xfail allows test to fail without failing the suite.
Repetition still happens, but failures are marked as expected.
"""
response = call_flaky_service()
assert response.ok
xfail above repeated means the entire repeated test is expected to fail.
With Custom Markers
@pytest.mark.slow
@pytest.mark.requires_gpu
@pytest.mark.repeated(times=30, threshold=27)
def test_gpu_accelerated_model():
"""
Custom markers (slow, requires_gpu) for test organization.
repeated marker at bottom ensures proper execution.
"""
prediction = gpu_model.predict(get_test_input())
assert validate_prediction(prediction)
Custom markers for organization/filtering should be above repeated.
With pytest-depends
import pytest
@pytest.mark.dependency(depends=["test_setup"])
@pytest.mark.repeated(times=50, threshold=48)
def test_that_depends_on_setup():
"""
Dependency marker ensures test_setup passed before this runs.
repeated marker at bottom ensures dependency check happens first.
"""
result = feature_requiring_setup()
assert result is not None
Dependency markers should be above repeated so dependencies are checked before repetition starts.
Multiple Repeated Tests
When multiple tests use @pytest.mark.repeated, each is independent:
@pytest.mark.repeated(times=100, threshold=95)
def test_feature_a():
assert feature_a_works()
@pytest.mark.repeated(times=50, threshold=48)
def test_feature_b():
assert feature_b_works()
Each test's repetitions are isolated - they don't affect each other.
Fixtures and Scope
Function-Scoped Fixtures (default)
@pytest.fixture
def fresh_database():
db = create_database()
yield db
db.teardown()
@pytest.mark.repeated(times=20, threshold=19)
def test_with_function_scope(fresh_database):
"""
Fixture runs 20 times (once per repetition).
Each repetition gets a fresh database.
"""
result = query_database(fresh_database)
assert result is not None
Module/Session-Scoped Fixtures
@pytest.fixture(scope="module")
def shared_database():
db = create_database()
yield db
db.teardown()
@pytest.mark.repeated(times=20, threshold=19)
def test_with_module_scope(shared_database):
"""
Fixture runs ONCE for the entire module.
All 20 repetitions share the same database.
"""
result = query_database(shared_database)
assert result is not None
Important: Module/session-scoped fixtures are not recreated between repetitions - all repetitions share the same instance.
Combining Multiple Approaches
You can combine basic, frequentist, and Bayesian parameters, but only one approach is evaluated:
# DON'T DO THIS - unclear which approach applies
@pytest.mark.repeated(
times=100,
threshold=95, # Basic approach
H0=0.90, ci=0.95, # Frequentist approach - conflicts!
)
def test_confusing():
pass
Best practice: Use only parameters for one statistical approach per test.
Anti-Patterns
❌ Repeated Inside Parametrize
# WRONG - unclear semantics
@pytest.mark.repeated(times=10, threshold=9)
@pytest.mark.parametrize("val", [1, 2, 3])
def test_wrong_order(val):
pass
Problem: Repeats the entire parametrized test set, not individual parameters.
Fix: Put parametrize first (above), repeated last (bottom).
❌ Nested Repetition
# WRONG - don't nest repeated markers
@pytest.mark.repeated(times=10, threshold=9)
@pytest.mark.repeated(times=5, threshold=4) # What does this even mean?
def test_double_repeated():
pass
Problem: Undefined behavior - pytest-repeated doesn't support nested repetition.
Fix: Use only one @pytest.mark.repeated per test.
❌ Assuming Fixture Runs Once
@pytest.fixture
def expensive_setup():
# Expensive operation
return setup_ml_model()
@pytest.mark.repeated(times=100, threshold=95)
def test_with_expensive_fixture(expensive_setup):
# WRONG assumption: fixture runs 100 times!
pass
Problem: Function-scoped fixtures run on every repetition (100 times here).
Fix: Use module/session scope for expensive fixtures:
Summary
| Scenario | Recommended Order | Example |
|---|---|---|
| Basic usage | repeated at bottom |
@repeated(...) |
| With parametrize | parametrize then repeated |
@parametrize → @repeated |
| With skip/xfail | skip/xfail then repeated |
@skipif → @repeated |
| With custom markers | Custom then repeated |
@slow → @repeated |
| With dependencies | dependency then repeated |
@dependency → @repeated |
Golden Rule: Keep @pytest.mark.repeated as the last decorator (closest to function) unless you have a specific reason not to.
Next Steps
- Basic Usage - Threshold-based testing
- Frequentist - Hypothesis testing approach
- Bayesian - Posterior probability approach
- Parameters Reference - All available parameters