Automated testing
Code that checks other code: a runner finds the tests, runs them and reports what failed, so every change is checked in seconds instead of by hand.
4 technologies on this page ↓How it works
A test is a small function that sets up an input, calls the code under test and asserts on the result. A test runner finds tests by naming convention (pytest collects test_*.py files and test-prefixed functions; Vitest collects *.test.ts and *.spec.ts files), runs them, and exits with a non-zero code if any assertion fails, so a CI job can block the merge. Runners usually run test files in parallel, in separate processes or worker threads so one file can’t leak state into another. Around that core sit helpers: mocks that stand in for a mailer or an API, snapshots that save an output to a file and fail when it changes, coverage reports, and a watch mode that reruns only the tests a saved file affects. Two layers build on the runner. A test environment manager such as tox or nox runs the same suite once per Python version or dependency set, each in a fresh virtual environment. A component workshop such as Storybook renders each UI component in isolation, one state per “story”, and turns those stories into tests in a real browser.
A pilot’s pre-flight checklist. The same items, in the same order, every time, quick enough that nobody skips it. Most checks are on single parts (unit tests); only a few are full test flights (end-to-end tests), because those are slow and expensive.
Reach for it when
- Code that several people change, or that you’ll change again in six months
- Libraries that promise to work on several language or dependency versions: run the suite once per version
- A bug you just fixed: reproduce it in a low-level test first, so it stays fixed
- UI components with many states (empty, loading, error, 1,000 rows) that are slow to reach by clicking through the app
Think twice when
- Throwaway scripts and spikes you’ll delete this week
- Leaning mostly on end-to-end tests through the UI: they are brittle, expensive to write and slow to run
- Snapshot tests nobody reviews: accepting a new snapshot accepts whatever changed
- Living with flaky tests: once people learn to dismiss red builds as noise, they dismiss real failures too
A real example
A team maintains a Python client library that supports Python 3.10, 3.11 and 3.12, plus a React dashboard. For the library, one tox.toml lists the three versions and a lint environment. tox run builds the package, installs it with pytest into a fresh virtual environment per version and runs the same tests in each: about 12 seconds on a laptop for a tiny suite, almost all of it environment setup. For the dashboard, Vitest runs the logic tests in about 100 ms and, in watch mode, reruns only the files a save affects. Each component state is a Storybook story, and the Vitest addon runs every story as a test in headless Chromium. CI runs all three and blocks the merge if any exits non-zero.
Kinds of automated testing
Specific products, and what each does differently.
Test runners →
Find the tests, run them (usually in parallel), and report the results with an exit code CI can act on.
Test environment managers →
Run the same suite in several isolated environments, one per interpreter or dependency version, from one config file.
Component workshops →
Render each UI component on its own, one state per story, then reuse the stories as documentation and as tests.
Also used for automated testing
Products of other kinds that are often used this way, grouped by what they are.
| Serverless functions (FaaS) | Commercial product | Cloudflare, Inc. | Proprietary (the Puppeteer and Playwright forks are open source) | 2026-10-12 |
The test pyramid#
For a long time, test automation meant tools that recorded clicks through an application’s user interface and played them back. Martin Fowler describes where that leads: tests that run end-to-end through the UI are brittle, expensive to write and slow to run, and a small change to the app can break many of them at once. A suite made mostly of such tests is the “ice-cream cone”.
The test pyramid turns it the other way up: many fast, focused unit tests at the bottom, fewer tests through a service or API layer in the middle, and only a few broad UI tests on top. Mike Cohn made the idea known as the “Test Automation Pyramid” in the 2009 book Succeeding with Agile, after first drawing it around 2003–2004. Fowler adds two caveats. The shape assumes broad tests are slow and brittle, so if yours are fast and reliable you need fewer low-level ones. And when a high-level test finds a bug, write a unit test that reproduces it before fixing it, so the bug stays dead.
Component tests sit between the layers. Storybook pitches them as the middle ground: the in-browser fidelity of an end-to-end test, with the scope of a unit test, since a component can still be given mocked data.
Flaky tests#
A flaky test passes and fails on the same code. Common causes are concurrency, reliance on undefined or non-deterministic behaviour, flaky third-party code and infrastructure problems. Google reported in 2016 that about 1.5% of all its test runs gave a flaky result, that almost 16% of its tests showed some flakiness, and that about 84% of the pass-to-fail transitions its CI saw involved a flaky test.
The cost is trust. With 1,000 tests and a 1.5% flaky rate, about 15 fail on any run for no reason, and people start dismissing real failures as noise. Automatic retries cut the noise but encourage people to ignore flakiness, and they delay real breaks: Google’s option to report a failure only after three failures in a row means a broken 15-minute test isn’t noticed for 45 minutes.
In code · python
# test_pricing.py: run with `pytest -q`
from unittest.mock import Mock
import pytest
def total(items, coupon=None):
s = sum(price * qty for price, qty in items)
return round(s * 0.9, 2) if coupon == "TEN" else s
# one test function, three cases: arrange the input, act, assert the result
@pytest.mark.parametrize(
("items", "coupon", "expected"),
[
([(2.5, 4), (1, 3)], None, 13),
([(20, 1)], "TEN", 18),
([], None, 0),
],
)
def test_total(items, coupon, expected):
assert total(items, coupon) == expected
def test_receipt_is_sent():
send = Mock() # a test double stands in for the real mailer
send(total([(5, 2)]))
send.assert_called_once_with(10)
# $ pytest -q
# .... [100%]
# 4 passed in 0.02s
# change the last case to expect 1 and the runner names it and exits non-zero:
# FAILED test_pricing.py::test_total[items2-None-1] - assert 0 == 1Often confused with
Where it came from1997–2025
Automated unit testing as most programmers know it began in Smalltalk. Kent Beck built small frameworks so that Beck and Beck’s clients could write tests in their normal Smalltalk environment and run a subset or the whole set quickly after every change. Beck preferred each team to build its own, which took a couple of hours. In 1997, on a flight from Zurich to the OOPSLA conference in Atlanta, Beck and Erich Gamma pair-programmed the first version of JUnit, test first. JUnit spread the idea with Extreme Programming and test-driven development, and ports followed for almost every language, CppUnit perhaps first; the family became known as xUnit.
As suites grew, the question became which tests to write. Mike Cohn sketched the test automation pyramid with Lisa Crispin around 2003–2004 and published it in 2009: many unit tests, fewer service tests, very few slow UI tests.
Later tools took the same loop in new directions. tox (2010) ran a Python package’s tests across several interpreters in virtualenvs. React Storybook (2016) rendered UI components in isolation, and from 2025 ran those stories as tests through Vitest, a runner first committed in December 2021 that reuses the Vite build pipeline.
Why it’s built this way
- Tests written in the same language as the code
- Beck’s focus was making it easy for programmers to define tests in their regular environment and run a subset or the full set quickly, so Beck’s teams ran the unit tests after every change.
- Many small tests, few broad ones
- Tests driven through the UI were brittle, slow and expensive to write. Pushing most checks down to unit tests keeps the suite fast and stable enough to run on every change.
- A fresh environment per version
- A library has to install and work on every Python version it claims. tox’s first release described itself as checking that a package installs correctly with different Python versions and interpreters.
- Stories as test cases
- A component workshop already lists every state of the UI, so adding assertions to those states lets the suite cover thousands of states with little extra code.
Timeline
- 1997
Beck and Gamma write the first JUnit on a flight to OOPSLA
- 2004
Mike Cohn describes the test automation pyramid at a Scrum gathering
- 2009
Cohn’s Succeeding with Agile publishes the pyramid
- 2010
tox 0.5 runs a package’s tests across Python interpreters in virtualenvs
- 2016
Kadira launches React Storybook
- 2021
First commit of Vitest, a test runner built on Vite
- 2025
Storybook 9 runs every story as a Vitest test in a real browser
- Martin Fowler: Xunit (Jan 2006)
- Martin Fowler: Test Pyramid (May 2012, etymology added Nov 2017)
- PyPI: tox 0.5 (July 2010), “virtualenv-based automation of test activities”
- Michael Shilman (Storybook blog): The Storybook story (May 2017)
- vitest-dev/vitest on GitHub: first commit by Anthony Fu (Dec 2021)
- Storybook blog: Storybook 9 (Jun 2025)
Sources14
- Martin Fowler: Test Pyramid (May 2012)
- Google Testing Blog: Flaky tests at Google and how we mitigate them (May 2016)
- pytest docs: Good integration practices (conventions for Python test discovery)
- pytest docs: How to parametrize fixtures and test functions
- Python docs: unittest.mock
- Vitest docs: include (default test file pattern)
- Vitest docs: Features
- Vitest docs: Parallelism
- Jest docs: Snapshot testing
- tox docs: Concepts
- nox docs: Welcome to Nox
- Storybook docs: Why Storybook
- Storybook docs: How to test UIs with Storybook
- Storybook blog: Storybook 9 (component tests between unit and E2E; Jun 2025)
