Data Engineering · lesson 12 of 23
Beginner
8- 1 Data Engineering Fundamentals
- 2 File Formats: CSV, JSON, Parquet
- 3 Idempotency and Safe Retries
- 4 Batch Pipelines and Time Windows
- 5 Partitioning and Physical Layout
- 6 Batch vs. Streaming: The Real Trade-off
- 7 Data Lake vs. Data Warehouse vs. Lakehouse
- 8 Data Modeling Basics: Star Schemas and Denormalization
Intermediate
4Advanced
11- 13 Streaming: Exactly-Once Semantics
- 14 Schema Evolution and Data Contracts
- 15 Streaming Architecture: Lambda and Kappa
- 16 Data Lineage: Tracing a Column Back to Its Source
- 17 Data Governance: Catalogs, Classification, and Access Policy
- 18 Data Security and PII: Hashing, Masking, and Tokenization
- 19 Pipeline Observability: Tracing, Anomaly Detection, and Alerting
- 20 Cost Optimization: Measuring What Partition and Column Pruning Actually Save
- 21 CI/CD for Data: Gating Promotion with Real Tests
- 22 Disaster Recovery: RPO, RTO, and What a Backup Actually Loses
- 23 Data Platform Architecture: Wiring the Pieces Together
Also part of Data Engineering
9- + Your First Airflow DAG from Airflow
- + Kafka Topics, Partitions, and Offsets from Kafka
- + Spark Architecture: Driver, Executors, and Cluster Managers from Apache Spark
- + Models, ref, and the DAG from dbt
- + Delta Lake: The Transaction Log from Databricks
- + The Medallion Architecture from Databricks
- + File I/O, pathlib, and JSON in Python from Python
- + Error Handling in Python from Python
- + Terraform Fundamentals (IaC) from Terraform
Also part of: Software Testing
Testing Data Pipelines
Write tests for a transform function, then let a 'harmless' refactor slip through code review and see the test suite catch it instead of production.
By SkillByExample · Published
Write four tests for two small transform functions, watch them pass, then let a “harmless” refactor slip past review and see which test catches it.
Two transforms, four tests
def clean_email(raw: str) -> str:
return raw.strip().lower()
def dedupe_orders(rows: list[dict]) -> list[dict]:
seen = set()
out = []
for row in rows:
if row["order_id"] not in seen:
seen.add(row["order_id"])
out.append(row)
return out
def test_clean_email_trims_whitespace():
assert clean_email(" Ada@Example.com ") == "ada@example.com"
def test_clean_email_lowercases():
assert clean_email("GRACE@EXAMPLE.COM") == "grace@example.com"
def test_dedupe_orders_keeps_first_occurrence():
rows = [{"order_id": 1, "amount": 10}, {"order_id": 1, "amount": 999}, {"order_id": 2, "amount": 20}]
result = dedupe_orders(rows)
assert len(result) == 2
assert result[0]["amount"] == 10, f"expected first occurrence (amount=10), got amount={result[0]['amount']}"
def test_dedupe_orders_empty_input():
assert dedupe_orders([]) == []
A tiny runner that reports pass/fail without pulling in a test framework:
def run_tests(tests):
passed = failed = 0
for name, fn in tests:
try:
fn()
print(f"PASS {name}")
passed += 1
except AssertionError as e:
print(f"FAIL {name}: {e}")
failed += 1
print(f"{passed} passed, {failed} failed")
run_tests([
("clean_email trims whitespace", test_clean_email_trims_whitespace),
("clean_email lowercases", test_clean_email_lowercases),
("dedupe_orders keeps first occurrence", test_dedupe_orders_keeps_first_occurrence),
("dedupe_orders handles empty input", test_dedupe_orders_empty_input),
])
$ python pipeline_testing.py
PASS clean_email trims whitespace
PASS clean_email lowercases
PASS dedupe_orders keeps first occurrence
PASS dedupe_orders handles empty input
4 passed, 0 failed
The “optimization” that changes behavior
Someone notices dedupe_orders builds a set and a list by hand and “simplifies” it to a
dict comprehension — same row count in, same row count out, code review approves it on sight:
def dedupe_orders(rows: list[dict]) -> list[dict]:
# "optimization": keep the last-seen row per order_id instead of the first
by_id = {row["order_id"]: row for row in rows}
return list(by_id.values())
== after the "optimization" ==
PASS clean_email trims whitespace
PASS clean_email lowercases
FAIL dedupe_orders keeps first occurrence: expected first occurrence (amount=10), got amount=999
PASS dedupe_orders handles empty input
3 passed, 1 failed
Both versions have the same row count, same input, same output shape — a reviewer scanning
the diff has no reason to suspect anything changed. The dict comprehension keeps whichever row
overwrites last, which for a duplicate order (a common CDC replay artifact — see the
incremental-loads-and-cdc lesson) is the exact opposite of “first occurrence wins,” and no
one asked which one was actually correct. The test did not know why it mattered; it only
had to know what the function was supposed to do, stated once, in advance.
Practice
1. Add a test asserting `clean_email` handles an already-clean address unchanged.
def test_clean_email_idempotent():
assert clean_email("ada@example.com") == "ada@example.com"
PASS clean_email idempotent
1 passed, 0 failed
A transform that is not idempotent on already-clean input usually indicates it is doing more than it claims — worth asserting explicitly rather than assuming.
2. Write a test for a `dedupe_orders` input containing zero duplicates.
def test_dedupe_orders_no_duplicates():
rows = [{"order_id": 1, "amount": 10}, {"order_id": 2, "amount": 20}]
assert dedupe_orders(rows) == rows
PASS dedupe_orders no duplicates
1 passed, 0 failed
The “no duplicates” case is the one a bug in the duplicate-handling logic is least likely to break, but it is also the cheapest to write — no reason to skip it.
3. Revert `dedupe_orders` to the first-occurrence version and confirm all four original tests pass again.
PASS clean_email trims whitespace
PASS clean_email lowercases
PASS dedupe_orders keeps first occurrence
PASS dedupe_orders handles empty input
4 passed, 0 failed
The test suite is the thing that makes “revert and confirm” a 5-second check instead of a manual re-read of the diff.
4. Which of the four original tests would have caught a bug where `clean_email` stripped whitespace but forgot to lowercase?
test_clean_email_lowercases specifically, and only that one — test_clean_email_trims_whitespace’s
input is already lowercase, so it would still pass even with a broken lowercasing step. This is
why each test should isolate one behavior: a test that happens to pass for the wrong reason
gives false confidence.
Next: streaming, exactly-once semantics — the same correctness problems with the clock running.