# Support code evolution with tests and decision records

Canonical page: https://yishto.com/en/learn/data-practice/testing-refactoring-and-decisions/

Author: [Yishto](https://yishto.com/en/about/)

Use an integer discount rule to define behavior, rounding boundaries, and test layers, then preserve the rationale for small refactors through decision records.

Published: 2026-11-10
Updated: 2026-11-10

A pricing function has accumulated conditions, and changing one discount rule affects other branches. Before restructuring it, define observable behavior: units, discount representation, rounding, and errors. Tests preserve those rules; decision records explain why the implementation was organized that way.

The case uses integer smallest currency units for textbook prices and basis points for discounts. Ten thousand basis points means a full discount; 1500 means fifteen percent. These are teaching rules, not payment-platform, tax, or regional currency specifications.

## Establish behavior before structure

Refactoring changes organization while preserving agreed observable behavior. Validation, results, exceptions, and side effects may all belong to the contract. Replacing an exception for an invalid price with zero changes behavior and requires a separate decision.

Inspect callers: are zero prices valid, can discounts reach one hundred percent, are units consistent, and are errors displayed? An undesirable existing rule can be revised, but distinguish that revision from a behavior-preserving refactor so billing does not change unnoticed.

Tests should express business examples and boundaries rather than repeat each implementation line. Known results protect the main rule; zero, full discount, no discount, and invalid types protect edges. A test that only counts private helper calls may reject a valid refactor without protecting user-visible results.

## Make units and rounding explicit

Binary floating point cannot represent some decimal amounts exactly. Integer smallest units avoid that representation issue, but rounding still needs a rule. This case floors the final amount using integer division; that is not a universal commercial convention.

The example rejects negative prices, invalid discounts, and nonintegers. Exact type checks exclude bool, which is a Python int subclass, so True cannot become a one-unit price.

```python
def discounted(price, basis_points):
    if type(price) is not int or price < 0:
        raise ValueError("invalid price")
    if type(basis_points) is not int or not 0 <= basis_points <= 10000:
        raise ValueError("invalid rate")
    return price * (10000-basis_points)//10000
assert discounted(1000, 1500) == 850
assert discounted(0, 1500) == 0
assert discounted(999, 0) == 999
try:
    discounted(-1, 1000)
    raise AssertionError("invalid input accepted")
except ValueError:
    pass
print(discounted(1000, 1500))
```

The actual output is `850`, the discounted amount for a price of 1000 and fifteen percent off. A negative price raises ValueError. Zero and no-discount examples are also checked. The code uses offline Python and does not run pytest, unittest, or a real billing service.

Add a remainder case: 999 with 1500 basis points returns 849 under this floor rule. A product may require nearest rounding, another convention, or line-level calculation before aggregation. Define applicable business rules and comparison cases before changing the calculation.

## Test different boundaries at suitable layers

Unit tests isolate pure rules and explain failures quickly. Integration tests verify database, service, and serialization boundaries. End-to-end checks cover a user path but depend more heavily on the environment. Choose according to risk instead of sending every arithmetic case through a browser.

Mocks control dependencies and can create timeout or denial cases. They can also hide contract mismatches if their fields differ from real service output. Keep focused checks at actual boundaries for inputs, outputs, statuses, and failures.

pytest offers assertions and fixtures, while unittest provides standard-library test organization, setup, and cleanup. Frameworks organize tests without selecting correct cases. Keep data reproducible and failure reports informative. Time and randomness need controlled conditions before they become assertions.

## Restructure around actual variation

One discount rule may need only a function. If regions or plans introduce variation, separate stable validation from replaceable calculation using composition or a strategy. Patterns provide vocabulary for change and coupling without requiring one class per value.

Move gradually: add contract tests, extract one variation point, run relevant checks, then update callers. Small changes are easier to diagnose and undo. Similar-looking code may have different business meanings, so verify whether it should evolve together before combining it.

Comments should explain non-obvious reasons, such as the floor rule, rather than narrate an assignment. Documentation needs prerequisites, units, inputs, results, errors, versions, and recovery behavior so callers can assess suitability.

## Retain the context of a decision

An ADR can record context, options, decision, status, and consequences. Here integers match existing input and settlement units, with the cost of explicit rounding and controlled conversion. Decimal is another option for decimal arithmetic but still needs precision and rounding management.

State revisit conditions, such as currencies with different smallest units or itemized taxes. If the decision changes, add a replacement record and mark the previous one superseded. “Integers are better” does not explain the tradeoff.

Open-source selection includes licensing, maintenance, compatible versions, scope, and replacement cost. Popularity alone is insufficient. Passing tests provides local behavioral evidence without establishing licensing or operating conditions, and does not justify claiming production use.

Failure tests should check the expected exception type. Catching every exception can mistake unrelated parsing or missing-field errors for correct rejection. Specify exception and message boundaries without depending on a full unstable stack trace. Verify cleanup on each branch so one case cannot leave state that changes the next result.

## Exercise: separate refactoring from rule changes

Add cases for zero, one unit, remainders, large prices, no discount, full discount, negative values, floats, and booleans. Document behavior, then extract a calculation strategy while preserving the existing contract.

Accept the refactor when valid inputs retain units and rounding, invalid inputs remain rejected, and callers cannot bypass validation. Propose a different rounding rule separately, with affected examples and a decision record. Keep before-and-after checks and a recoverable commit. Data migrations need additional compatibility and restoration verification.

## Sources & further reading

- [Writing tests with pytest](https://docs.pytest.org/en/stable/getting-started.html)
- [Pro Git](https://git-scm.com/book/en/v2)
- [Documenting architecture decisions](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions)
- [Unit testing framework](https://docs.python.org/3/library/unittest.html)

Sources checked on 2026-11-10. Examples are teaching data, not production measurements.

## Related guides

- [Make a change reviewable and recoverable with Git](https://yishto.com/en/learn/developer-workflow/git-ssh-and-review/)
- [Make AI-assisted development produce checkable results](https://yishto.com/en/learn/product-building/specifications-and-coding-agents/)
