Skip to main content
Clean Architecture 70 XP · 7 min

Programming as Science: Falsifiability and Testing

Tests don't prove correctness — they prove absence of known incorrectness. Good architecture produces easily falsifiable modules.

Showing
Ad (728×90)

Why this matters

Dijkstra showed that testing can never prove a program correct — only that it hasn't been found incorrect yet. This shifts the scientific framing: programs are falsifiable hypotheses, and a test suite is the ongoing attempt to falsify them. A passing test suite doesn't mean your code is right; it means it hasn't been proven wrong yet.

This has a direct architectural consequence: if a module cannot be falsified easily, it is an architectural failure. Code that mixes I/O, parsing, validation, and network calls in a single function has too many reasons to be wrong and too many dependencies to be tested in isolation. Good architecture forces the important logic into pure, isolated units that can be probed from any angle with any input — making the hypothesis easy to attempt to falsify.

The problem

A function that reads a file, parses it, validates data, and sends email cannot be unit-tested without a real file system and a live SMTP server. It is architecturally unfalsifiable.

Bad

import smtplib, csv

def process_user_file(path: str) -> None:
    with open(path) as f:
        for row in csv.DictReader(f):
            if "@" not in row["email"]:
                raise ValueError(f"Bad email: {row['email']}")
            with smtplib.SMTP("smtp.example.com") as smtp:
                smtp.sendmail("no-reply@app.com", row["email"], "Welcome!")
async function processUserFile(path: string): Promise<void> {
  const rows = parseCSV(fs.readFileSync(path, "utf8"));
  for (const row of rows) {
    if (!row.email.includes("@")) throw new Error(`Bad email: ${row.email}`);
    await smtp.sendMail({ to: row.email, subject: "Welcome!" });
  }
}

The solution

Extract the core logic into a pure function. Now it is fully falsifiable — any input, any environment, no infrastructure required.

Good

def is_valid_email(email: str) -> bool:
    """Pure function — testable with any input, no I/O, no side effects."""
    return "@" in email and "." in email.split("@")[-1]

# Tests need nothing but the function itself
assert is_valid_email("user@example.com") is True
assert is_valid_email("not-an-email")     is False
assert is_valid_email("missing@dot")      is False
function isValidEmail(email: string): boolean {
  const [local, domain] = email.split("@");
  return !!local && !!domain && domain.includes(".");
}

// Falsifiable with any input — no mocks, no network, no files
console.assert(isValidEmail("user@example.com") === true);
console.assert(isValidEmail("not-an-email")      === false);

Key takeaway

A function that cannot be tested in isolation is an architectural defect — the inability to falsify it is the symptom, and poor separation of concerns is the cause.

Done with this lesson?

Mark it complete to earn XP and track your progress.