Skip to content
PHP2Shell: The PHP Bug
Akshay Thakare

Testing Data Pipelines with pytest

What to test in a pipeline, how to build tiny hand-checked fixtures, and the tests that catch real production bugs: replays, duplicates and bad rows.

PythonIntermediate2 min readUpdated 11 Oct 2026

Test behaviour, not lines of code

The bugs that hurt in production are rarely syntax errors. They are behaviours:

BehaviourTest
Re-running a batchOutput is identical (idempotent)
Duplicate source rowsCounted once
Bad rowsRejected or quarantined, not silently dropped
Failure half-wayNothing partial is published
Late dataLands in the right partition

A fixture you can check by hand

Python.py
import pytest
 
@pytest.fixture
def orders():
    return [
        {"order_id": "O1", "qty": 2, "price": 500},   # 1000
        {"order_id": "O1", "qty": 2, "price": 500},   # duplicate resend
        {"order_id": "O2", "qty": 1, "price": 300},   # 300
    ]
 
def test_duplicates_counted_once(orders):
    assert total_sales(dedupe(orders)) == 1300

Keep fixtures small enough that the expected number fits in a comment. If you need a calculator to check a test, the fixture is too big.

Test idempotency directly

Python.py
def test_replay_changes_nothing(tmp_path):
    run_pipeline(tmp_path, batch="2026-01-05")
    first = read_facts(tmp_path)
    run_pipeline(tmp_path, batch="2026-01-05")
    assert read_facts(tmp_path) == first

tmp_path gives each test its own directory, so tests never share state.

Inject failures

Python.py
def test_failure_rolls_back(tmp_path, monkeypatch):
    def boom(*args, **kwargs):
        raise RuntimeError("injected")
    monkeypatch.setattr(pipeline, "publish", boom)
    with pytest.raises(RuntimeError):
        run_pipeline(tmp_path, batch="2026-01-05")
    assert read_facts(tmp_path) == []

Finished this note?

Share