Some code is genuinely hard to test not because testing itself is hard, but because the code’s design actively fights against it — a handful of patterns make the difference between a five-minute test and a genuinely frustrating one.
# HARD TO TEST: hidden dependencies, side effects buried inside the function
def process_order(order_id):
order = database.query(f"SELECT * FROM orders WHERE id={order_id}") # hidden DB call
if datetime.now().hour > 22: # hidden time dependency
send_email("Processing overnight order") # hidden side effect
payment_api.charge(order.total) # hidden external call
return order
# TESTABLE: dependencies passed in explicitly, easy to substitute with fakes
def process_order(order, current_time, email_service, payment_service):
if current_time.hour > 22:
email_service.send("Processing overnight order")
payment_service.charge(order.total)
return order
The Patterns That Actually Help
- Dependency injection — pass in dependencies rather than reaching out to global state or creating them internally
- Pure functions where possible — same input always produces the same output, with no side effects, trivially testable with no setup at all
- Separate decision logic from side effects — a function that decides what to do can be tested without actually doing it
If writing a test for a piece of code feels disproportionately painful, that’s frequently a genuine signal about the code’s design, not just a testing tooling problem — treating that friction as useful design feedback, rather than something to work around, tends to produce better code overall, not just more testable code.