Skip to main content

Sign in to CleanKata

Track your progress, earn XP, and unlock every lesson.

By signing in you agree to our Terms of Use and Privacy Policy.

Clean Architecture70 XP7 min

Programming as Science: Falsifiability and Testing

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

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.

🔧 Some exercises may still have errors. If something seems wrong, use the Feedback button (bottom-right of the page) to report it — it helps us fix it fast.

Hint: A function that can't be tested in isolation is an architectural problem, not a test problem.

✗ Your version

Programming as Science: Falsifiability and Testing — CleanKata — CleanKata