Why can a pure renewal core be tested without standing anything in for the clock or the account store?
answer
- nothing to intercept, nothing to supply
- the fixture is just data
- arguments in, decisions out
- no setup, no ordering, no reset
- the outer layer still needs wiring tests
basics
~20 sNothing needs standing in because the core never calls out: the instant and the account records arrive as ordinary arguments. A test builds those values, calls the function once, and compares the decisions it returns.
solid answer
~40 sA substitute exists to intercept a call, and the core makes none. Its inputs are records and an instant handed in by the caller, and its output is a list of decisions, so a case is literally `given these values, expect these decisions`. That means no fixture, no setup or teardown, no ordering between cases and nothing to reset, so the cases read as a table and run in any order or at once. A failure reproduces from the arguments printed in the report. What it does not cover is the wiring: whether the outer layer loaded the right rows and performed every decision it got back still needs a smaller, slower set of tests at the boundary.
code
pseudocode · 12 linestest "an account paid through yesterday is charged today"
account = { id: 7, paidThrough: day(10), price: 900, cancelledAt: null }
now = day(11)
decisions = decide([account], now)
assert decisions == [ { kind: "CHARGE", accountId: 7, amount: 900 } ]
test "a cancelled account is left alone"
account = { id: 8, paidThrough: day(10), price: 900, cancelledAt: day(9) }
assert decide([account], day(11)) == []go deeper
Notice what the test does not contain: no fixture, no setup, no waiting. If every input is an argument and the output is a value, the case is just a comparison of two values.
Explain why a substitute has nothing to intercept here, and say what stays for the outer layer's own cases — that it loaded the right inputs and performed every decision returned.
Argue the balance: many cheap cases over the rules, a deliberate handful over the wiring and boundary. Name the bugs the cheap ones structurally cannot see rather than claiming coverage.
Treat test cost as a design signal across teams: when new rules keep arriving with slow cases attached, decisions are landing outside the core, and that trend is worth watching as a number.
## Why there is nothing to stand in for A substitute is something you supply so that a call the code makes goes somewhere you control. A pure decision core makes no such call. Its dependencies are not reached for — they were **handed in**: the account records, the price, the instant, any random value already drawn. There is nothing to intercept, so there is nothing to supply. That changes what a test *is*. Instead of arranging a world and then asserting about it, the test states a relation between two values: - **Given** these records and this instant - **Expect** exactly these decisions The whole fixture is data, and the assertion compares data. That is why this style of test tends to collapse into a table of cases: each row is inputs and expected output, and adding a rule means adding rows. ## What you stop paying for - **Setup and teardown.** There is no world to build or dismantle, so cases have no prologue. - **Ordering.** No case can leave residue for the next, because nothing was written. - **Flakiness from time.** The instant is a literal, so a case that exercises a renewal boundary behaves the same in March and in December. - **Reproduction cost.** A failing case is fully described by its arguments; pasting them back reproduces it exactly. - **Concurrency limits.** Cases can run at once, because they share nothing. One caution against overstating it: fast and repeatable is not the same as *correct*. A core test proves the rule does what you said, not that what you said is the rule the business wanted. ## Where the remaining tests go | what is checked | where it runs | character | |---|---|---| | the rules themselves | core, over literal values | fast, repeatable, many cases | | wiring: right rows loaded, every returned decision performed | outer layer | fewer, slower, needs a boundary | | the boundary itself behaves as assumed | against a real dependency | fewest, slowest | The split is the point: the layer with the most cases is the one with the lowest cost per case, and the layer that costs the most per case carries the fewest. Before the rules were isolated, both roles belonged to the same slow tests, which is why suites in that shape grow minutes and then hours. ## What the fast cases cannot see Be precise about the hole you are leaving, because it is real: - The outer layer loading the wrong set of accounts — the core happily decides about whatever it is given. - A returned decision that is dropped, or performed twice after a retry. - A field mapped wrongly on the way in, so the core reasons about a plausible but wrong record. - The dependency at the boundary behaving differently from your assumption. None of those are rule bugs, and none of them will ever turn a core test red. They are exactly why a handful of tests over the outer layer stay worth their cost, and why "we moved the logic into a pure core" is not an argument for deleting them. ## A useful side effect on the design Writing these tests exerts steady pressure back on the arrangement. If a case needs a fixture, something in the path reads; if a case has to run before another, something writes; if a case is hard to state as values, the function is probably deciding and acting at once. In that sense the test difficulty is a measurement, not an obstacle — a rule that suddenly needs a stand-in is telling you a read has crept back into the core, usually the day someone needed one more record mid-decision. Finally, the arrangement makes a stronger kind of check affordable. Because the core is a function of its arguments, you can assert relations across many generated inputs — that no cancelled account is ever charged, that the total charged equals the sum of prices of exactly the due accounts — without building a world for each one. That style is expensive against a real boundary and nearly free against a pure function.
- What kinds of bug do these fast core cases never catch?Everything at the boundary: loading the wrong set of accounts, dropping a returned decision, performing one twice after a retry, a field mapped wrongly on the way in. The core can be flawless while the program is wrong, so a small number of slower cases over the outer layer stay worth their cost.
- Why is a suite of pure-core cases cheaper to run repeatedly?Each case is a call over values, so there is no setup, no teardown, no ordering between cases and nothing shared to reset. They can run in any order or at once, and a failure reproduces from the arguments in the report rather than from the state the machine happened to be in.
saying these in an interview costs you the question
- Insists a pure function still needs a stand-in for time
- Believes fast core cases make boundary cases unnecessary
- Wraps a function that touches nothing in setup and teardown
- Moves a rule outward to reuse an existing fixture
- Treats repeatable results as evidence the rules are right