Static Testing and Reviews
The cheapest defects to fix are the ones found before the code runs. Requirement reviews, code review as testing, and what a linter or type checker is really doing for you.
The cheapest defects to fix are the ones found before the code runs. Requirement reviews, code review as testing, and what a linter or type checker is really doing for you.
The review took forty seconds. "Looks good to me, maybe rename
data to payload." Approved.
Eleven days later the basket page started returning a 500 for every customer who removed their last item, because line 34 divided a total by the number of items and nobody had asked what happens when that number is zero. The question would have taken one comment. Nobody had to run anything to ask it.
By the end of this lesson you will know what static testing covers, how to review a requirement and a change so that it finds things, and what a linter or a type checker is doing on your behalf. If the cost curve from the first lesson was persuasive, this is where you act on it.
Dynamic testing executes the software. Everything in the previous lessons was dynamic.
Static testing examines the artefacts without executing them: requirements, designs, code, configuration, documentation, and test cases themselves.
It is testing in the sense that matters — you are examining something in order to find defects — and it reaches things dynamic testing cannot. A requirement that contradicts itself cannot be found by running the software, because software built from a contradictory requirement runs perfectly and does the wrong thing.
Everything that is wrong before it runs.
Ambiguous and contradictory requirements. The case nobody considered. Dead code. A variable used before it is set. A type mismatch. A design that cannot be tested. Code the next person will not be able to maintain.
Everything that only exists at run time.
Actual wrong behaviour. Performance under load. Integration failures. The half of the world that no amount of reading can predict.
The single highest-value static activity, and the one the last lesson but two prepared you for: reading a requirement to find what it does not say.
Six things to look for, each of which produces a real question:
Ambiguity. A word that two people could read differently. "Recent orders" — recent meaning what? "The system should be fast" — compared to what, measured how?
Contradiction. Two statements that cannot both hold. One section says sessions expire after 30 minutes; another describes a "keep me signed in" option that lasts a fortnight. Which wins?
Incompleteness. The rule with no failure case. "The user uploads a CSV and the records are imported" — and if row 400 of 500 is malformed? All-or-nothing, or partial with a report?
Untestability. A requirement nobody could verify. "The interface should be intuitive" cannot pass or fail. Turn it into something observable: "a new user completes their first upload without help in under three minutes."
Unstated assumptions. "Show the user's orders" assumes the user has orders. What does the screen look like with none?
Missing non-functional requirements. How many users at once? How large may the file be? What must be logged? Who may see this data? Almost never written down, always decided by someone.
Reviews come in several weights, and matching the weight to the stakes is the practical skill. The width below is how often you should be reaching for each.
Assigned roles, recorded metrics, a formal defect log. Days. Only where a defect is genuinely unacceptable — medical devices, aviation, payments infrastructure.
Peers check the work against standards. Hours. Designs and architecture, occasionally.
The author talks it through with the room. An hour. For building a shared understanding, not for finding typos.
"Can you look at this?" Minutes. Almost all of everyday work, and where nearly all the defects get caught.
What separates a review that works from one that wastes an hour is not formality but three habits: people prepared in advance rather than reading it in the meeting, defects recorded rather than debated to a conclusion in the room, and the author not defending — a review examines the work, and the author's job is to answer questions about it, not to justify it.
Most teams review code, and most reviews are not used as testing. They check style, naming and structure, all of which matter, and stop short of the question a tester would ask.
That question is: what inputs would break this? Reviewing a change with the shapes from the earlier lesson in mind turns a code review into a cheap defect hunt:
That last line is worth insisting on. A change with tests that only demonstrate success is a change with no tests, in the sense that matters.
Static analysis is the automated half: tools that examine code without running it, on every change, and find a specific class of defect better than any human.
Linters encode rules about likely mistakes and inconsistency. A
variable assigned and never used, an unreachable branch, a missing
await, a comparison that is always true. ruff for Python, eslint
for JavaScript and TypeScript.
Type checkers verify that values are used consistently with their
declared types. A function that says it returns a string and sometimes
returns None is a bug that a type checker finds instantly and a test
suite may never — because the test data never hit that branch.
Formatters remove an entire category of review comment by settling
formatting mechanically. black, prettier, gofmt.
Security and dependency scanners look for known-vulnerable dependencies, hard-coded credentials, and unsafe patterns.
Complexity metrics point at functions that are unusually branchy — which, from the bug-clustering argument, is where to test hardest.
All of it belongs in the pipeline, failing the build. A rule that is advisory is a rule that gets ignored under deadline pressure, and then the codebase drifts and the tool becomes noise.
Bad — a review comment that improves nothing measurable:
Good — the same time spent asking what would break:
The first review is not neutral — it is actively costly. It consumes the author's time, it produces an approval that implies the change was examined, and it teaches the team that review is a formality. The one genuine defect on line 34 ships, and when it surfaces in production everybody can point at an approved review.
The second takes ten minutes and finds a crash, a boundary question and a missing test. None of those needed the software to run.
Requirements, before anything is built
The cheapest defect removal available anywhere. One question can delete a week of work built on a misunderstanding.
Test cases, before anyone runs them
An ambiguous case wastes every future run of itself. A missing case costs nothing until the day it matters, and then costs everything.
Code, on the day it is written
Cheap because the author still has the whole change in their head. The same comment three weeks later costs an afternoon of re-learning first.
Configuration and infrastructure
A misconfigured environment variable, an over-broad permission, a security group open to the world. Unglamorous, catches expensive things, and much of it can be automated.
Static testing catches what can be found by reading. The next lesson is the opposite discipline: exploratory testing, where you deliberately work without a script, and which finds the bugs no written case was ever going to.
Before that, review one requirement you have been given, hunting the six things above, and write down every question it raises. Take those to whoever owns it. That conversation is the cheapest testing you will do this month.
Inputs what happens with empty, zero, missing, negative, huge,
or a string where a number is expected?
State what if this runs twice? What if the previous attempt
half-finished?
Errors is every failure handled, or does one path swallow an
exception silently?
Boundaries is that comparison < or <=? Should it be?
Permissions is authorisation checked here, or assumed to have
happened earlier?
Tests do the tests accompanying this change include a
failure case, or only the happy path?Looks good to me. Maybe rename `data` to `payload`?
Approved.Line 34: if `items` is empty, `total / len(items)` divides by
zero. Is an empty basket possible here?
Line 51: this is `>` — should a quantity of exactly the limit be
allowed? The requirement says "up to 100".
The tests cover a successful import. Is there one for a
malformed row? That is the case the ticket was about.
Otherwise this reads well.# Static: examine without executing. Dynamic: run it.
static finds ambiguity, contradiction, missing cases, dead code,
type errors, untestable designs, unmaintainable code
dynamic finds wrong behaviour, performance, integration failures
# Reviewing a requirement — six things to hunt for
ambiguity "recent", "fast", "large", "appropriate"
contradiction two statements that cannot both be true
incompleteness a rule with no failure case
untestability "intuitive" — make it observable and measurable
assumptions it assumes the user HAS orders. And if not?
missing NFRs load, size limits, logging, who may see this
# Formality — match it to the stakes
informal "can you look at this?" everyday
walkthrough the author talks it through shared understanding
technical review peers against standards designs, architecture
inspection roles, metrics, defect log safety-critical only
# What makes any review work
# people prepared BEFORE the meeting
# defects recorded, not debated to a conclusion in the room
# the author answers questions rather than defending
# Code review, as testing — the questions to actually ask
inputs empty, zero, missing, negative, huge, wrong type?
state what if this runs twice? after a half-finished attempt?
errors every failure handled, or one silently swallowed?
boundaries is that < or <=? Should it include the limit?
permissions checked here, or assumed to have happened earlier?
tests is there a failure case, or only the happy path?
# Automate the mechanical half, and fail the build on it
ruff check . lint Python
mypy . type-check Python
eslint . lint JavaScript / TypeScript
tsc --noEmit type-check TypeScript
npm audit known vulnerable dependencies
# advisory rules get ignored under pressure; make them blocking
# Where it pays most
1. requirements, before anything is built
2. test cases, before anyone runs them
3. code, on the day it is written
4. configuration and infrastructureruff check . # lint Python
mypy . # type-check Python
eslint . # lint JavaScript/TypeScript
tsc --noEmit # type-check TypeScript
npm audit # known vulnerable dependencies