Why we test
Software is a set of decisions written down as instructions, and people make mistakes when writing decisions down. Testing is the practice of running the software, or parts of it, under chosen conditions and comparing what happens with what was expected. The goal is to find out whether the work behaves as intended before someone depends on it.
It helps to be modest about what testing can do. A test can show that a problem exists. It cannot show that no problems exist, because the number of possible inputs and situations is almost always too large to try them all. A passing test suite is evidence of confidence, not proof of correctness.
Testing also has a second purpose that is easy to overlook: it protects change. When a project has checks that run quickly, an engineer can modify code and learn within minutes whether something else broke. Without them, every change carries more worry, and people tend to avoid improving code that works but is awkward.
What a test actually checks
Every test, however large, has the same basic shape:
- Set up a starting situation.
- Do something to the software.
- Compare the result with an expected outcome.
The expected outcome is the important part. It comes from somewhere: a written requirement, an example agreed with a customer, a calculation done by hand, or the behaviour the code had last week that nobody wants to lose. If nobody can say what the right answer is, the test cannot be written, and that is often a sign that the requirements engineering behind the feature is incomplete.
Tests can check different kinds of things:
- Functional behaviour: given this input, does the software produce this output or action?
- Error handling: what happens with empty values, wrong types, missing files or a network that does not answer?
- Non-functional qualities: speed, memory use, resilience and similar properties. These are harder to test, but non-functional requirements are often what users notice first.
- Boundaries: values at the edge of what is allowed, such as zero, the largest permitted value, or an empty list. Mistakes cluster at boundaries.
Test levels
Tests are usually grouped by how much of the system they involve. The names vary between teams, but the idea is consistent.
Unit tests
A unit test exercises one small piece of code, such as a single function, in isolation. It runs fast, usually in a fraction of a second, and when it fails it points to a small area. Because they are cheap, a project can hold many of them.
Integration tests
An integration test checks that several pieces work together: code talking to a database, one module calling another, or software communicating with a device. Many bugs live in the gaps between components rather than inside them, which is why separately tested parts can still fail when joined.
System tests
A system test runs the whole product as a user or another system would see it, ideally in an environment close to the real one. These tests are the most realistic and the slowest, and when they fail the cause can be hard to locate.
Acceptance tests
Acceptance tests ask whether the software does what the customer or stakeholder needs. They are tied to agreed acceptance criteria and often decide whether a piece of work counts as finished.
A common way to picture the balance is a pyramid: many fast, small tests at the bottom; fewer integration tests in the middle; a small number of whole-system tests at the top. The shape is a rule of thumb, not a law, and the right mix depends on the project. A system that is mostly glue between services may need proportionally more integration testing than a maths library.
Designing useful tests
A good test is easy to read, fails for one clear reason and does not depend on the order in which tests run. It should check behaviour that matters rather than internal details that may change. A test that breaks every time the code is reorganised, even though nothing visible changed, costs more than it gives.
Some habits help:
- Name tests after the behaviour they check, so a failure message is readable on its own.
- Keep each test focused on one idea.
- Make tests repeatable. A test that passes sometimes and fails sometimes teaches people to ignore failures.
- Include the awkward cases (empty, zero, very large, malformed) alongside the typical ones.
Writing tests before the code is one approach some teams use; test-driven development is covered separately, along with the situations where it fits poorly.
Measuring and automating
Teams often want a number that says how thorough their tests are. Test coverage is the common choice, and it is useful as long as its limits are understood: it reports which lines ran, not whether the results were checked.
Running tests by hand does not last. Once there are more than a handful, they are normally run automatically on every change, which is the idea behind continuous integration. Automation turns testing from an occasional event into a routine safety net.
When a test fails
A failing test is information. Sometimes the code is wrong, sometimes the test is wrong, and sometimes the expectation has changed. The first job is to find out which, using the same patient approach described in material on debugging strategies. When a failure turns out to be a genuine defect found by someone else, a clear set of bug reports makes the fix much quicker.
For physical products, the same thinking applies on the bench, with sensors and instruments in place of assertions; testing hardware prototypes has its own planning concerns.
Testing within the wider quality picture
Testing is one part of quality assurance, not the whole of it. Reviews, clear requirements, sensible design and careful release practice all prevent defects that testing would otherwise have to find later. Teams that rely on testing alone tend to find problems late, when they are more expensive to fix.
Where software affects safety, health, money or legal obligations, testing needs to follow the standards and professionals relevant to that industry and country. Requirements differ widely, and nothing in a general guide replaces the judgment of a qualified engineer or the rules that apply to your product.
A starting point
If your project has no tests yet, begin small. Pick one important behaviour, write one repeatable check for it, and make it run on every change. Add checks when you fix a bug, so the same mistake is harder to repeat. Over time the habit matters more than any single technique.