Software Engineering
Unit Testing
Code written, manually verified, deployed. A week later - a production bug in that exact function. "But I checked it!" The difference between "checked manually" and "there are tests" is the difference between hope and confidence. Unit tests do not remove bugs - they make them impossible to reintroduce.
- **Google** requires 80%+ branch coverage for production code; every PR must add tests for new code paths
- **Netflix** practices chaos engineering - deliberate failures in production. This is only possible with a solid test foundation
- **NASA** used formal verification and testing for Mars Rover: no bug in flight software can be patched after launch
Arrange-Act-Assert Pattern
A test without structure is a puzzle. It is unclear what is being set up, what is being tested, and what the expected result is. **Arrange-Act-Assert** (AAA) provides three explicit phases that turn a test into a readable specification of behavior.
**Arrange** - prepare everything needed: objects, data, mocks. **Act** - perform exactly one action: the function or method call under test. **Assert** - verify the result or side effect. One function = one test = one hypothesis under verification.
**Single assert phase rule:** if a test has multiple unrelated assertions - split into multiple tests. When a test fails, it will be immediately clear what broke.
What belongs in the Act phase?
Mocks: When and How
A unit test verifies one unit in isolation. But real code depends on databases, HTTP services, file systems. A **Mock** replaces a dependency, controls input data, and verifies how the tested code interacts with it: which methods were called, with what arguments, how many times.
A Mock differs from other test doubles by containing **expectations**: the test checks both the result and that the code communicated correctly with the dependency. This is useful when the communication itself matters, beyond the outcome.
**When a mock is excessive:** if a test mocks 5 dependencies, the class likely does too much (SRP violation). Mock-heavy tests are a signal to rethink the architecture.
A Mock differs from a Stub in that:
Stubs vs Fakes vs Spies
Testing has a family of **test doubles** - replacements for real dependencies. They are often confused under the label "mocks". Each type solves a different problem, and choosing the right one makes a test simpler.
| Type | Real Logic? | Verifies Calls? | Use When |
|---|---|---|---|
| Stub | No | No | Controlling input data |
| Fake | Yes (simplified) | No | Logic needed without external deps |
| Spy | Yes (real) | Yes | Verifying a side effect on real object |
| Mock | No | Yes | Verifying communication with a dependency |
Testing a service with a repository. Only the service logic matters; repository data is fixed. Which test double fits?
Code Coverage: Metrics and Traps
100% code coverage does not mean an absence of bugs. Coverage measures which lines of code executed during tests, but not whether the expected behavior is correct. Tests can be written with zero `expect()` calls - coverage will show 100%, the tests will be meaningless.
Four coverage types: **statement** (lines), **branch** (if/else paths), **function** (functions), **line** (lines with adjacent statements). The most useful in practice is **branch coverage**: it forces testing both paths of every condition.
**Practical rule:** 80% branch coverage in critical modules (business logic, finance) is a reasonable target. 100% everywhere often causes more harm than good: the team writes tests to hit the metric, not to gain confidence.
100% code coverage means reliable and correct code.
Coverage shows which code executed, not whether behavior is correct. A test with no expect() gives 100% coverage.
The coverage metric incentivizes writing tests for lines, not for confidence. Branch coverage plus meaningful assertions matter - not the number.
A project has 95% statement coverage. This guarantees:
Unit Testing
- AAA (Arrange-Act-Assert): one test checks one thing; three explicit phases make a test a readable specification
- Mock verifies interaction (how many times called, with what arguments); use when the communication itself matters
- Stub, Fake, Spy - different doubles for different jobs; choose the simplest one that fits
- Coverage is a code execution metric, not quality; branch coverage beats statement; 80% in critical modules is a sane target
Related Topics
Unit tests are the base of the testing pyramid:
- Code Review — Tests are the first thing checked in review
- CI/CD Pipelines — Tests run in CI on every push
- Refactoring — Tests give confidence when refactoring
Вопросы для размышления
- Where in the current project do tests provide real confidence, and where do they just cover lines for the metric?
- How to tell whether code has become harder to test because of architecture, rather than problem complexity?
- Why are tests for edge cases (null, empty list, max value) more valuable than happy-path tests?