Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
A unit test tests one thing in isolation: one function, one class method, one business rule. The word 'isolation' is load-bearing: if your test touches a database, makes an HTTP call, or depends on another module's state, it is not a unit test — it's a small integration test. This distinction matters because isolated unit tests run in microseconds (you can run 10,000 per second), pinpoint exactly which piece of code broke (no need to debug which layer failed), and require no infrastructure (no test database, no network). The skill of writing testable code — structuring it so its dependencies can be replaced with fakes — is what separates SDETs from automation engineers who only test finished UIs.
Dependency injection is the design pattern that makes unit testing possible: instead of importing a database client directly, the function accepts it as a parameter, which lets tests pass a lightweight in-memory fake instead. Without this seam, every test would need a real database, real credentials, and real data — making the suite slow, environment-dependent, and fragile. Structuring code around injected dependencies is the single most impactful change an SDET can champion in a codebase.
import pytest
# ✗ NOT a unit test — it calls the real database
# def test_user_exists():
# from db import get_user # imports the real module → real DB connection
# user = get_user(1)
# assert user.name == "Alice"
# ✓ Unit test — the dependency is injected (can be replaced with a fake)
class UserService:
def __init__(self, user_repo): # dependency injection via constructor
self._repo = user_repo
def get_display_name(self, user_id: int) -> str:
user = self._repo.find_by_id(user_id)
if user is None:
raise ValueError(f"User {user_id} not found")
return f"{user['first']} {user['last']}".title()
def is_premium(self, user_id: int) -> bool:
user = self._repo.find_by_id(user_id)
return user is not None and user.get("tier") == "premium"
# ── Tests ────────────────────────────────────────────────────────────────────
class FakeUserRepo:
"""In-memory fake — no database, no network, no I/O."""
def __init__(self, users: dict):
self._users = users
def find_by_id(self, user_id: int):
return self._users.get(user_id)
def make_service(users=None):
repo = FakeUserRepo(users or {1: {"first": "alice", "last": "smith", "tier": "premium"}})
return UserService(repo)
def test_display_name_title_cases_correctly():
svc = make_service()
assert svc.get_display_name(1) == "Alice Smith"
def test_display_name_raises_for_missing_user():
svc = make_service(users={}) # empty repo — user doesn't exist
with pytest.raises(ValueError, match="User 1 not found"):
svc.get_display_name(1)
def test_premium_user_is_identified():
svc = make_service()
assert svc.is_premium(1) is True
def test_free_user_is_not_premium():
svc = make_service(users={1: {"first": "bob", "last": "jones", "tier": "free"}})
assert svc.is_premium(1) is False
def test_missing_user_is_not_premium():
svc = make_service(users={})
assert svc.is_premium(99) is False
# Run: pytest -v → should show 5 passing tests with clear namespython3 main.pyformatEmailForDisplay(userId: int) -> str that returns a***@example.com (first char + *** + domain). Write 3 unit tests: valid email, missing user, email with no @ character. Each test should be independent — the others shouldn't need to pass for any one to pass.FakeUserRepo to raise an exception: raise ConnectionError('DB down'). Which tests fail? Which pass? This shows that unit tests should fail only when the unit under test is broken — not when its dependency fails.from database import db). Change it to accept the dependency as a parameter. Write the before/after tests. How did testability change?Use these three in order. Each builds on the one before.
In one paragraph, explain what 'isolation' means in unit testing and why a test that makes a real database call is not a unit test. What would you replace the database with to make it a unit test?
Walk me through dependency injection in the context of unit testing: what is it, how does it work in Python (constructor injection vs function parameter), and how does it enable testing a class without its real dependencies?
I'm adding unit tests to a 3-year-old codebase where every function directly imports database connections, HTTP clients, and file system calls. The code has no dependency injection. Walk me through a refactoring strategy to make the code testable: which files to start with, what the refactoring steps are, how to ensure you don't break production behavior during the refactoring, and how to measure progress.