Software testing basics: 50 cards on TDD, mocks and the test pyramid
Software testing fundamentals: the test pyramid, unit, integration and E2E tests, TDD, mocks vs stubs vs fakes, coverage, mutation and property-based testing.
Practice now
Study without an account
Scheduled with the default Askesis timings. Progress is saved in this browser.
All 50 cards
- 1The test pyramidMany fast unit tests at the base, fewer integration tests in the middle, and a few end-to-end tests at the topTests get slower, costlier and more brittle toward the top, so most checks belong lower down. Popularized by Mike Cohn.
- 2Unit testTests a small piece of code, such as a function or a class, in isolation; it runs in milliseconds, without a network, database or file system
- 3Integration testChecks that several components work together correctly, e.g. your data-access code against a real database, or two services talking over HTTP
- 4End-to-end (E2E) testExercises the whole running system the way a user would, e.g. driving a browser with Playwright or Cypress through sign-up and checkoutThe most realistic kind of test, but also the slowest and the most prone to flakiness.
- 5The “ice cream cone” anti-patternAn upside-down test pyramid: mostly manual and end-to-end tests with few unit tests, which makes feedback slow and flaky
- 6The testing trophyKent C. Dodds’ alternative to the pyramid: static analysis at the base, then unit tests, a large layer of integration tests, and a few end-to-end tests on topIts motto, from Guillermo Rauch: “Write tests. Not too many. Mostly integration.”
- 7Arrange, Act, AssertA common structure for a test: set up the data, perform the one action under test, then check the outcomepython
def test_coupon_reduces_total(): cart = Cart([Item(price=100)]) # Arrange cart.apply_coupon("SAVE10") # Act assert cart.total() == 90 # Assert - 8Black-box vs white-box testingBlack-box tests are designed from the specification without looking at the code; white-box tests are designed from the code’s internal structure, e.g. to cover every branch
- 9Smoke testA quick, shallow check that the most important things work at all (the app starts, the home page loads, a user can sign in) before deeper testing
- 10Regression testingRe-running tests after a change to make sure existing behavior still worksWhen you fix a bug, first add a test that reproduces it, so the same bug can’t silently come back.
- 11Acceptance testingChecks that the system meets the business requirements or acceptance criteria, usually from the user’s point of view; in user acceptance testing (UAT), real users or the customer do the checking
- 12The TDD cycleRed: write a failing test. Green: write just enough code to make it pass. Refactor: clean up while the tests stay greenTest-driven development was popularized by Kent Beck.
- 13Benefits of test-driven developmentYou define the behavior before writing code, the design is pushed toward small, decoupled, testable units, and you end up with a test suite that makes refactoring safe
- 14Behavior-driven development (BDD)Describing behavior as concrete examples in business-readable language, usually Given / When / Then scenarios that run as tests, e.g. with CucumberGiven a cart containing one book When the customer applies the coupon SAVE10 Then the total is 10% lower
- 15Test doubleAny object that stands in for a real dependency in a test; the umbrella term for dummies, stubs, spies, mocks and fakesThe term comes from Gerard Meszaros, by analogy with stunt doubles in films.
- 16Dummy (test double)An object passed only to fill a parameter list and never actually used
- 17StubA double that returns canned answers, so the test controls what the code under test receives, e.g. an exchange-rate service that always returns 1.1
- 18SpyA double, or a wrapper around a real object, that records how it was called so the test can check those calls afterward
- 19MockA double programmed with expectations about the calls it should receive; the test fails if the interactions don’t matchIn everyday speech “mock” means any double, and libraries such as Jest and
unittest.mockcreate objects that can act as stubs, spies or mocks. - 20FakeA real, working implementation that takes a shortcut unsuitable for production, e.g. an in-memory repository instead of a database
- 21Stubs vs mocks: state vs behavior verificationWith stubs you check the result or final state (state verification); with mocks you check that the right calls were made (behavior verification)Heavy mocking ties tests to implementation details, so harmless refactors break them. See Martin Fowler’s essay “Mocks Aren’t Stubs”.
- 22What does this test use the
Mockfor?pythonfrom unittest.mock import Mock def test_checkout_charges_card(): gateway = Mock() gateway.charge.return_value = {"status": "ok"} checkout(Cart(total=4200), gateway) gateway.charge.assert_called_once_with(4200)Both roles:return_valuemakes it a stub that feedscheckouta canned reply, andassert_called_once_withverifies the call, as a mock would - 23“Don’t mock what you don’t own”Instead of mocking a third-party library directly, wrap it in a small interface of your own, use doubles of that interface in unit tests, and cover the wrapper with integration testsA mock of code you don’t control encodes your assumptions about it, which may be wrong or go out of date.
- 24Line, branch and function coverageThe share of lines executed, of decision outcomes taken (both sides of every
if), and of functions called while the tests run - 25The only test calls
discount(100, member=True). Line and branch coverage?pythondef discount(price, member): if member: price = price * 0.9 return price100% line coverage, but only 50% branch coverage: the path wherememberis false never runs - 26Why doesn’t 100% code coverage mean the code is well tested?Coverage only shows that code ran, not that its results were checked: a test without assertions still counts, and missing cases or edge inputs don’t show upWhen coverage becomes a target, people write tests to hit lines rather than to catch bugs (Goodhart’s law). Use it to find untested code, not as a quality score.
- 27Mutation testingA tool makes many small changes (mutants) to your code, such as
>to>=or+to-, and runs the tests against each one. A mutant that no test catches reveals a gap in the testsTools: PIT (Java), Stryker (JavaScript, C#, Scala), mutmut (Python), cargo-mutants (Rust). - 28Killed vs survived mutants, and the mutation scoreA mutant is killed if at least one test fails, and survives if all tests still pass. The mutation score is the share of mutants killedMutation testing is slow, since the suite runs once per mutant, and some mutants behave exactly like the original code, so no test can kill them.
- 29Property-based testingInstead of hand-picked examples, you state a property that must hold for all inputs, and the framework checks it against hundreds of generated inputsIt started with QuickCheck for Haskell; Hypothesis (Python) and fast-check (JavaScript) are popular today.
- 30What kind of test is this?python
from hypothesis import given, strategies as st @given(st.lists(st.integers())) def test_sorting_twice_changes_nothing(xs): assert sorted(sorted(xs)) == sorted(xs)A property-based test: Hypothesis generates many lists of integers and checks, for every one, that sorting is idempotent - 31Shrinking (property-based testing)After finding a failing input, the framework simplifies it step by step to the smallest input that still fails, e.g. from a 40-element list down to
[0, -1] - 32Useful properties to testRound trips (
decode(encode(x)) == x), invariants (sorted output is ordered and has the same length), idempotence (doing it twice equals doing it once), and agreement with a simpler reference implementation - 33Flaky testA test that sometimes passes and sometimes fails on the same codeFlaky tests teach a team to ignore red builds, so fix or quarantine them quickly.
- 34Common causes of flaky testsFixed sleeps and timing assumptions, race conditions in async code, shared state or dependence on test order, and relying on the real clock, randomness or network
- 35How do you stop a UI test from failing on a slow page load?Wait for a condition, such as the element becoming visible, with a timeout, instead of sleeping for a fixed timePlaywright’s locators and web-first assertions wait automatically.
- 36Test isolationEach test sets up and cleans up its own state, so it passes on its own, in any order and in parallel with othersCommon tools: a database transaction rolled back after each test, temporary directories, and no shared mutable globals.
- 37How do you test code that depends on the current time?Control the clock: inject a clock object, or use fake timers such as
jest.useFakeTimers()or Python’sfreezegunReading the real clock directly makes results depend on when the tests run, e.g. failing only around midnight or on leap days. - 38Contract testingChecks that an API consumer and its provider agree on requests and responses by testing each side separately against a shared contract, without deploying both together
- 39Consumer-driven contract testing (e.g. Pact)The consumer’s tests record the requests it makes and the responses it needs into a contract; the provider’s CI replays that contract against the real provider and fails on breaking changes
- 40Test fixtureThe fixed setup a test needs, such as objects, data or database state, often shared through hooks like
beforeEachor pytest fixtures - 41Parameterized testOne test function that runs with several sets of inputs and expected outputspython
import pytest @pytest.mark.parametrize("a, b, expected", [(1, 2, 3), (0, 0, 0), (-1, 1, 0)]) def test_add(a, b, expected): assert add(a, b) == expected - 42Boundary value analysisTesting at and just around the edges of valid ranges, where off-by-one bugs live: for a valid range of 1–100, try 0, 1, 2, 99, 100 and 101
- 43Equivalence partitioningDividing inputs into groups the code should treat the same way, such as negative, zero and positive, and testing one representative of each group instead of every value
- 44Snapshot testingSaving the output of a component or function and failing whenever it changes laterCheap to write, but snapshot changes are easy to approve without reading; keep snapshots small.
- 45Characterization testsTests that pin down what legacy code currently does, right or wrong, so you can refactor it safelyDescribed by Michael Feathers in “Working Effectively with Legacy Code”.
- 46The FIRST properties of good unit testsFast, Independent, Repeatable, Self-validating (they pass or fail with no manual checking), Timely (written along with the code)
- 47Why test behavior rather than implementation details?Tests that check observable results through the public interface survive refactoring; tests of private methods or internal calls break whenever the code is reorganized, even when it still works
- 48Load vs stress vs soak vs spike testingLoad: the expected traffic. Stress: beyond capacity, to find the breaking point. Soak: normal load for hours or days, to find leaks. Spike: a sudden surge
- 49Fuzz testingFeeding huge numbers of random or malformed inputs to a program to find crashes, hangs and security bugsCoverage-guided fuzzers such as AFL++ and libFuzzer keep mutating the inputs that reach new code paths. Go has fuzzing built into
go test. - 50Functional vs non-functional testingFunctional tests check what the system does (its features and rules); non-functional tests check how well it does it: performance, security, usability, reliability
Frequently asked questions
What is the difference between a mock and a stub?
A stub returns canned answers so the test controls the input; a mock records calls, and the test checks that the expected interactions happened. Many libraries create objects that can do both.
What code coverage should I aim for?
There is no universal number. Use coverage to find untested code, and judge tests by whether they would catch real bugs; mutation testing measures that more directly.
Which language are the examples in?
Python, with pytest, unittest.mock and Hypothesis, but the ideas apply to any language and test framework.
More in Computer science
Computer science
CS fundamentals: binary and hex, two’s complement, Big-O, stack vs heap, threads and deadlocks, CPU caches, compilers, UTF-8 and floating point.
50 cardsMorse code
Learn International Morse code: all 26 letters, the 10 digits, common punctuation and signals like SOS, each with its rhythm and a memory aid.
50 cardsDS & Algorithms
Big-O for arrays, hash maps, heaps, trees and graphs, sorting trade-offs, and the patterns behind interview problems: two pointers, sliding windows, BFS, Dijkstra and DP.
50 cardsDesign patterns
SOLID, DRY, KISS and YAGNI, plus the Gang of Four patterns used most: singleton, factory, builder, adapter, decorator, observer, strategy, command and more.
50 cardsAPI design
API design fundamentals: REST, HTTP methods, status codes, pagination, versioning, OAuth 2.0 and JWT, CORS, OpenAPI, GraphQL, gRPC and webhooks.
50 cardsSystem design
System design fundamentals: load balancers, caching, CDNs, replication and sharding, CAP, consistency, queues, rate limiting and back-of-the-envelope math.
50 cardsCryptography
Cryptography flashcards: AES modes, RSA, elliptic curves, Diffie–Hellman, hashing, HMAC, signatures, password hashing, TLS 1.3 and common pitfalls.
50 cards