Verdict
No
Do not write unit tests for branchless one-line wrappers and delegations that pass one value or values of different types; test the code that calls them.
Why
- Blast radiususers
- Change frequencyrarely
- Detectabilityimmediately
- Reversibilitytrivial
- Test costtrivial
Do not write unit tests for a branchless one-line function that passes one value or values of different types; test the code that calls it. For the typical case, a wrapper, a delegation, or a one-line expression whose callers have tests in CI, the factors are: Blast radius is users, Change frequency is rarely, Detectability is immediately, Reversibility is trivial, and Test cost is trivial. Detectability is immediately because a wrong one-liner fails its callers' tests in the same CI run. Reversibility is trivial because a revert leaves no data behind. Rule R8 gives Do not test, and the case sits on the edge: if Detectability moves to same-day, the decision becomes Test minimally.
| When | Decision | Why |
|---|---|---|
| The one-liner converts a price to integer cents, such as `int(amount * 100)` | Test mandatory, with amounts such as 19.99 that binary floats cannot hold exactly | Blast radius rises to money, Detectability moves to eventually and Reversibility to costly: a missing cent looks plausible on every invoice |
| The one-liner is an access check that compares the user's organisation ID with the document's | Test mandatory: one allowed case and one denied case | Blast radius rises to safety-or-legal and Detectability moves to never: a wrong comparison shows other customers' records silently |
| No test runs the code that calls the one-liner | Test minimally: one test of the main path, on the caller or on the one-liner | Detectability moves to same-day: the first report of a wrong result comes from a user |
| The one-liner forwards several arguments of one type, such as `return Range(end, start)` | Test: one test that passes a distinct value for each argument and checks where each one lands | Detectability moves to eventually: a swapped argument produces a plausible value of the right type |
| The one-liner wraps a library call, such as a pandas method, and dependency upgrades land every month | Test: one test that pins the wrapper's output for a representative input | Change frequency rises to regularly and Detectability moves to eventually: an upgrade can change a default without a change in your code |
What breaks if you don't test
For a wrapper whose arguments differ in type and whose callers are tested, nothing breaks that CI misses: a swapped argument fails the type check or the caller's test before the merge. The failures that reach users come from one-liners that only look trivial: a float conversion that drops a cent, a toISOString() call that returns the UTC date instead of the user's local day, a filter that compares the wrong ID, a Range(end, start) call that swaps two dates.
What you lose if you over-test
A test for full_name() that asserts "Ana Lima" repeats the line it checks, so it fails only when someone changes that line on purpose, and every such change touches two files. The test adds no coverage either, because the callers' tests already execute the line.
What to do instead
Test the behaviour at the level of the caller: the endpoint response, the report row, the computed total. Run a type checker such as mypy, which rejects a wrapper that returns the wrong type. Then open the coverage report once: if the caller's tests execute a one-liner whose arguments differ in type, the one-liner needs nothing more. The coverage.py documentation shows how to produce a line-by-line report.
When the answer changes
- The line converts, rounds, or compares values that stand for money, dates, or permissions.
- The one-liner forwards two or more arguments of one type, such as two dates or two user IDs.
- The one-liner wraps a library, and dependency upgrades land every month.
Code example + Counterexample
The one-liner that drops a cent
This function has one line and no branches, and it is wrong:
def to_cents(amount: float) -> int:
return int(amount * 100)
def test_to_cents_keeps_every_cent():
assert to_cents(19.99) == 1999 # fails: to_cents returns 1998
In binary floating point, 19.99 * 100 evaluates to 1998.9999999999998, and int() cuts off the fraction, so the charge is one cent short. Nothing crashes and the amount looks plausible, so Detectability is eventually; with Blast radius at money and Reversibility at costly, the framework returns Test mandatory for int(amount * 100). The fix is round(amount * 100), or Decimal amounts from the first input; the Python floating-point tutorial explains why 19.99 has no exact binary value.
Related questions
FAQ
- Should I unit test trivial functions?
No, a one-line function without branches needs no unit test of its own when the tests of its callers run it in CI. A one-liner that converts money, compares permissions, handles dates, or forwards several arguments of one type gets a test, because its failure is silent.
- Do I need to unit test functions with no control flow?
No, a function with no control flow needs no dedicated unit test when its callers are tested. Test a branchless function when its output is money:
int(amount * 100)has no branch and loses a cent on 19.99.- Should I unit test thin wrappers around a library API?
No, a thin wrapper around a library that you upgrade a few times a year needs no test of its own. When dependency upgrades land every month, add one test that pins the wrapper's output, because an upgrade can change a library default silently.
- Should I delete tests that are too simple to break?
No, keep an existing test for a trivial function unless every edit of the function forces an edit of the test. A test on code that rarely changes costs almost nothing to keep, and a test written during TDD has design value that the framework does not count.