Python Testing Pyramid 2026: The Complete Problem-Solving Guide
By Huzi
File #560 · 4 min read
The Friday bug ships not because a team had no tests, but because the integration test had been silently skipping itself for three weeks while CI glowed green. Bad test architecture is the wrong tests in the wrong layer — no tooling fixes that.
The quick answer: shape the suite as roughly 70% unit, 20% integration, 10% end-to-end, and keep the inner loop under five seconds so people run it. pytest, pytest-asyncio, Hypothesis and testcontainers carry every layer — they need the right shape.
The 70/20/10 shape
Roughly seventy percent unit tests: fast, isolated, every external dependency mocked, each done in under a second. Twenty percent integration: a real database and real schemas, catching what mocks cannot.
Ten percent end-to-end is reserved for flows that must never break — signup, checkout, login. The inverted pyramid is the common failure: a 45-minute browser suite that flakes and teaches everyone to ignore red.
| Layer | Share | What lives there | Speed target |
|---|---|---|---|
| Unit | ~70% | Isolated logic, dependencies mocked | Under a second each |
| Integration | ~20% | Real database, real schemas | Seconds |
| End-to-end | ~10% | Signup, checkout, login | Minutes, nightly |
Fast tests get run. Slow tests get skipped. Hold the line.
pytest carries the load
Plain assert statements, fixtures for setup and teardown, parametrize for data-driven cases, markers for filtering. A fixture uses @pytest.fixture; yield instead of return, and code after the yield is teardown that runs even when the test raises.
import pytest
@pytest.fixture
def sample_user() -> dict:
return {"id": 1, "name": "Huzi", "email": "[email protected]"}
@pytest.mark.parametrize("a, b, expected", [
(2, 3, 5), (-1, 1, 0), (0, 0, 0),
])
def test_add(a: int, b: int, expected: int) -> None:
assert add(a, b) == expected
Shared fixtures live in conftest.py, discovered automatically. Register custom markers in pyproject.toml, then run pytest -m "not slow" for the fast loop and pytest -m slow nightly — the inner loop survives and the expensive paths still run.
Mock the boundaries, never the internals
Mock what your code does not control: external APIs, the clock, the filesystem. Mocking internal collaborators couples tests to the implementation, so every refactor breaks them for nothing.
unittest.mock.patch is the standard tool; pytest-mock's mocker fixture is the ergonomic wrapper that cleans up after itself.
def test_get_user_ip(mocker) -> None:
mock_get = mocker.patch("myapp.api.requests.get")
mock_get.return_value.json.return_value = {"ip": "203.0.113.7"}
assert get_user_ip() == "203.0.113.7"
mock_get.assert_called_once_with("https://api.ipify.org")
assert_called_once_with is the safety net: not just "was it called" but "with exactly these arguments" — how you prove the payment gateway got the right order ID without a real gateway. freezegun pins datetime.now() and turns flaky time-dependent tests deterministic.
Boundaries deserve the same suspicion in travel paperwork: a passport six months short or a transit rule misread is a runtime error at the airport. HTG Travels verifies real requirements before you book.
The layers that catch real bugs
Hypothesis generates hundreds of cases from one function — empty lists, huge integers, Unicode you would never type — and shrinks any failure to the smallest input that still fails. Three properties pin down a sort completely: output is sorted, length is preserved, no element invented.
testcontainers spins up a real PostgreSQL in a throwaway Docker container, so repository tests catch the schema drift SQLite politely hides. In CI, pytest -n auto --cov=src --cov-report=html parallelizes across cores and hands you a coverage report.
Pro tip: Turn
branch = Trueon in coverage config — line coverage lies about anifthat only ran one way.
Verification is an act of respect, and I think of Gaza's students sitting exams in tents — may the world one day grade itself as honestly as a good test suite.
Before we submit any visa file, someone re-checks every page against the checklist — passport validity, photo size, the meningococcal certificate — because an error caught at the desk costs nothing and one at the embassy costs the trip.
That is the discipline HTG Travels runs on — bring us the file refused elsewhere; finding out why is our favourite first step. When the deploy can wait and the mountains cannot: +92 317 5054959.




