Unit & Integration Tests
The fast layer: frameworks, test doubles that do not lie, and fixtures that keep a suite deterministic.
A Suite People Trust
Three properties make the difference. Tests must be deterministic — same input, same result, every run. Independent — any order, any subset, no shared state. And fast enough that people run them before pushing rather than after.
Test Frameworks
Every ecosystem has settled on the same shape: a runner that finds tests, assertions that state expectations, and hooks that set up and tear down. The names differ; the structure does not.
| Ecosystem | Common choice |
|---|---|
| Python | pytest |
| JavaScript | Jest or Vitest |
| Java | JUnit 5 |
| C# | xUnit |
| Go | the standard library testing package |
def test_expired_token_is_rejected():
# arrange
token = make_token(expires_in=-60)
# act
result = auth.verify(token)
# assert
assert result.ok is False
assert result.reason == "expired"
Arrange, act, assert is worth following literally. A test with the three phases visible is readable at a glance, and a test that needs two "act" steps is usually two tests.
test_expired_token_is_rejected tells you what broke from the CI output alone. test_verify_2 means opening the file.Mocking & Test Doubles
A test double stands in for a real collaborator so a test stays fast and deterministic. They are not interchangeable, and using the wrong one is how suites end up asserting their own implementation.
| Double | Is |
|---|---|
| Stub | Returns canned answers |
| Fake | A working lightweight implementation — an in-memory store |
| Mock | Asserts that it was called in a particular way |
| Spy | Records calls for you to check afterwards |
The useful rule: mock what you do not control and what is slow — third-party APIs, the clock, randomness, the network. Use real objects for your own code wherever it is fast enough, and prefer a fake over a mock when you need behaviour rather than a call assertion.
Fixtures & Test Data
Test data decides how much of a suite is readable and how much of it is flaky. The failure mode is always the same: tests that share state and pass alone but fail together.
| Practice | Why |
|---|---|
| Build data per test | Shared fixtures couple tests to each other |
| Factories over literals | Say what matters, default the rest |
| Reset between tests | Truncate or roll back — never rely on ordering |
| Freeze the clock | A test that fails at midnight or in February is a real bug you cannot reproduce |
| Seed randomness | Reproducibility beats variety in a CI run |
# a factory says only what the test cares about
user = make_user(plan="pro") # everything else defaulted
# freeze time rather than computing against "now"
with freeze_time("2026-01-01T12:00:00Z"):
assert subscription.is_active(user) is True
Interview Questions
What makes an automated suite trustworthy?
Determinism, independence and speed. A suite that fails randomly trains everyone to rerun it, and the one real failure ships along with the noise.
Stub, fake, mock, spy — what is the difference?
A stub returns canned answers, a fake is a working lightweight implementation, a mock asserts how it was called, and a spy records calls for later inspection.
What is the risk of over-mocking?
The test ends up asserting your own implementation. Everything passes while the system is broken, because nothing real was exercised.
What should you mock?
What you do not control or what is slow — third-party APIs, the network, the clock, randomness. Use real objects for your own code while it is fast enough.
Why freeze the clock in tests?
Otherwise behaviour depends on when the suite runs — tests that fail at midnight, at month end, or in a different timezone, and cannot be reproduced on demand.
A test passes alone but fails in the suite. Why?
Shared state — a database row, a module-level cache, an environment variable. Tests must build their own data and reset after themselves.