# Good: Pure function, easy to testdefcalculate_discount(price:float,tier:str)->float:rates={"bronze":0.05,"silver":0.10,"gold":0.15}returnprice*rates.get(tier,0)deftest_calculate_discount():assertcalculate_discount(100,"gold")==15.0assertcalculate_discount(100,"unknown")==0.0
1
2
3
4
5
6
7
# Bad: Depends on database, time, external servicedefget_user_discount(user_id):user=db.get(user_id)# DB dependencyifuser.signup_date<datetime.now()-timedelta(days=365):# Time dependencybonus=loyalty_service.get_bonus(user_id)# External dependencyreturnbonus*1.5return0
fromtestcontainers.postgresimportPostgresContainer@pytest.fixture(scope="session")defpostgres():withPostgresContainer("postgres:15")aspg:yieldpg.get_connection_url()deftest_with_real_postgres(postgres):engine=create_engine(postgres)# Test against real Postgres
# Bad: Assumes immediate consistencyuser_service.update(user_id,name="New Name")user=user_service.get(user_id)assertuser.name=="New Name"# Might fail if async# Good: Wait for condition or use synchronous pathuser_service.update_sync(user_id,name="New Name")user=user_service.get(user_id)assertuser.name=="New Name"
Time dependencies:
1
2
3
4
5
6
7
8
9
10
11
# Bad: Depends on wall clockdeftest_expires_after_24h():token=create_token()time.sleep(86401)# Don't do thisasserttoken.is_expired()# Good: Inject timedeftest_expires_after_24h(freezer):token=create_token()freezer.move_to(datetime.now()+timedelta(hours=25))asserttoken.is_expired()
Order dependencies:
1
2
3
4
5
6
7
8
9
10
11
# Bad: Test depends on another test's statedeftest_create_user():create_user("alice")deftest_get_user():user=get_user("alice")# Fails if test_create didn't run first# Good: Each test sets up its own statedeftest_get_user():create_user("bob")# Own setupuser=get_user("bob")
# pytest.ini[pytest]markers =flaky:mark test as flaky (deselect with '-m "not flaky"')# test_something.py@pytest.mark.flakydef test_sometimes_fails():...
Run flaky tests separately, fix them, or delete them.
test:stage:testparallel:matrix:- TYPE:unit- TYPE:integration- TYPE:e2escript:- pytest tests/$TYPE -v --junitxml=results-$TYPE.xml# Run unit tests first (fast feedback)unit_tests:stage:testscript:- pytest tests/unit --timeout=10timeout:5minutes# Integration tests after unit passintegration_tests:stage:testneeds:[unit_tests]services:- postgres:15script:- pytest tests/integrationtimeout:15minutes# E2E last, only on main branche2e_tests:stage:testneeds:[integration_tests]rules:- if:$CI_COMMIT_BRANCH == "main"script:- pytest tests/e2etimeout:30minutes