Why tests matter and what kinds exist
You fixed one function, shipped the update — and a day later a user writes that something else broke, something that worked yesterday. A change in one place quietly affected another, and you were the last to find out.
Testing is how you learn about breakages like this right away, not from users. It's a set of checks that confirm your code does what it's expected to — and keeps doing it after every change.
Why test at all
In 1999, NASA lost a $125 million Mars orbiter because of a simple mistake: the calculations mixed different unit systems. A single test could have caught the mismatch before launch.
That's an extreme case of a general rule — the later a bug is found, the more it costs to fix:
Beyond catching bugs early, tests give you a few more things:
- confidence when changing code — with a test suite you can refactor and add features boldly: if something breaks, the tests are the first to tell you;
- living documentation — tests show how the code is meant to be used and, unlike ordinary docs, never go stale;
- better design — if a function is hard to test, it's usually just built too complex;
- protection from regressions — that's the name for breakage of previously working code after new changes; the example below catches exactly those.
Types of tests
Tests differ in the scale of what they check — from a single function to the whole system.
Unit tests
They check the smallest, isolated parts of the program — individual functions, methods or classes. The goal is to make sure every "brick" of the code works correctly on its own.
Python 3.13def add(a, b): return a + b print(add(2, 3) == 5)True
The check returned True — the function works as expected. Now imagine someone edited add and accidentally broke the logic:
Python 3.13def add(a, b): return a + b + 1 # a bug that slipped in print(add(2, 3) == 5)False
The same check instantly returned False. That's what a unit test is: a small check that tells you by itself whether the function is intact — that's how regressions get caught. In the next article we'll see how to write such checks with assert and run them in batches.
Integration tests
They check how several modules work together: for example, that after a user is created in the database, the login system recognizes them. Modules can be fine on their own and still fail to fit together — that's what gets caught here.
End-to-end tests (E2E)
They check the whole system from the user's point of view, passing through every layer of the application. An example is the full signup scenario: fill in the form, submit the data, receive the email, log in.
The testing pyramid
How many of each kind should you write? The usual answer is drawn as a pyramid:
The higher the level, the slower and more expensive the tests — so there are fewer of them. The bulk is fast unit tests: they point exactly at the place of the breakage.
Principles of a good test (FIRST)
Five traits of a useful test spell the acronym FIRST:
- Fast: slow tests get run rarely;
- Independent: doesn't rely on other tests or the run order;
- Repeatable: gives the same result on every run;
- Self-validating: tells you by itself whether it passed, with no manual comparison of results;
- Timely: written together with the code, not "someday later".
Testing frameworks
Writing checks by hand, as in the add example, gets old fast: you want batch runs, reports, convenient comparisons. That's what frameworks are for, and Python has two main ones:
- pytest — third-party, with a concise syntax; that's where we'll start;
- unittest — built into the standard library.
Understanding check
Which statement about software testing is the most accurate?
In the next article — hands-on practice with pytest: your first real tests, the assert statement and batch runs.
