# 0023. Unit tests without external dependencies

- **Status:** Accepted
- **Date:** 2026-08-08

## Context

The project had no automated tests at all. Every change was verified by hand, which is workable for layout and
copy but not for the arithmetic behind offers, rentals and payments — the `ProposalRequestItemCalculator` alone
carries three separate rounding and calendar rules, and nothing was checking any of them.

The obvious first instinct — a full test environment with a fixture database, a bootstrapped request and
integration tests over controllers — was rejected before it was started. A suite like that takes tens of
seconds, needs its own schema kept in sync with the migrations, and is exactly the kind of thing that stops
being run. The suite is only worth having if it runs before every push without anyone thinking about it.

There is no coverage driver in the PHP container (neither xdebug nor pcov), and adding one was declined: it
means rebuilding the image, and the number it produces is not the point.

## Decision

**One suite: PHPUnit unit tests with no external dependency.** No database, no HTTP, no cache, no session, no
mail, no filesystem writes.

- `tests/bootstrap.php` starts a `yii\web\Application` carrying only `formatter`, `i18n` and `urlManager`.
  **The `db` component is deliberately absent**, and a test asserts that `Yii::$app->db` throws.
- What is tested is a **curated allowlist**, declared in the `<source>` block of `phpunit.xml.dist` and
  mirrored, with the partial cases and their reasons, in [TESTING.md](../TESTING.md). "Fully covered" means
  every public method has a test — verified by reading the class, not by a coverage report.
- Code that cannot be tested without a database stays out of the list rather than dragging a database in.
- PHPUnit is a **dev dependency**; production installs with `--no-dev` and never runs tests.
- Running the suite is a written rule, not a git hook: before a commit when the change touches the tested
  surface, **always before a push**. A weekly review keeps the list honest.

## Alternatives

- **Integration tests over a fixture database** — the highest-value coverage on paper, and the reason there
  were no tests before. Too slow to run before every push, and the fixtures become a second schema to
  maintain. Not ruled out forever, but it would be a second suite with its own rules, not this one.
- **Codeception**, which Yii 2 ships templates for — a heavier toolchain for acceptance and functional testing,
  most of which we are explicitly not doing. Plain PHPUnit is what is left once those layers are removed.
- **pcov plus a coverage threshold** — a percentage is easy to satisfy without testing anything meaningful, and
  it would have made the container image a prerequisite for running the suite. A named list of files is
  smaller, honest, and readable in the diff.
- **A `pre-push` git hook** — rejected as a mechanism the developer did not want in the way. The rule lives in
  [WORKFLOW.md](../WORKFLOW.md) instead.

## Consequences

- The suite runs in well under a second, so there is no excuse not to run it.
- Coverage is a maintained list, not a measurement. It only stays true if the weekly review actually happens;
  that is the failure mode to watch.
- Making something testable sometimes means a small refactor — widening a pure static helper's visibility, or
  decoupling a calculation from an ActiveRecord. Those are allowed, and are recorded in the task that needs
  them.
- Writing the first tests immediately surfaced six production defects, three of them in the money arithmetic
  (a daylight-saving off-by-one in the billed day count, an under-counted full month that also picked the
  wrong price tier, and a precision-losing `int` conversion). That is the argument for the suite, and the
  reason the list should keep growing.
