Which part of a distributed job can a plain unit test call without a cluster, and what stops it?
answer
- two kinds of code in one file
- the rule, not the wiring
- signature decides testability
- plain values in, plain values out
- no runtime type, no ambient reads
basics
~20 sThe per-record rule can: a plain function taking ordinary values and returning ordinary values. It stops being callable once its signature mentions a runtime type, or it reads configuration, storage or the clock itself instead of taking them as arguments.
solid answer
~50 sA job file holds two kinds of code. One is wiring - where the input is, how records are grouped or joined, where the output goes. The other is the rule for a single record: which ones are dropped, how a field is cleaned, how two fields become a third. A cluster exists to run that rule over more records than one machine can finish; it does not change what the rule computes on one record. So lift the rule into a plain function of the host language - a record in, a record or nothing out, no runtime type in the signature, no configuration read, no connection opened, no wall clock - and a test can call it thousands of times in a second with no cluster. What is left in the wiring should carry no business rule at all.
code
python · 17 lines# liftable: plain values in, plain values out, nothing ambient
def settle(record: dict, rates: dict[str, float]) -> dict | None:
if record.get('amount') is None:
return None
rate = rates.get(record['currency'])
if rate is None:
return None
return {**record, 'settled': round(record['amount'] * rate, 2)}
# the test needs no cluster and no network
assert settle({'amount': 10.0, 'currency': 'x'}, {'x': 2.0})['settled'] == 20.0
assert settle({'amount': None, 'currency': 'x'}, {'x': 2.0}) is None
# the wiring, kept free of business rules, is the only part that needs machines:
# it locates the input, applies settle to every record of every piece,
# builds the rates table once per worker, and names the output location.go deeper
Recall the split: wiring names inputs, grouping and outputs; the rule says what one record becomes. The rule is an ordinary function, and an ordinary function is what a unit test calls.
Explain the signature test - plain values in and out, nothing read from configuration, storage or the clock - and show what is left in the wiring once every business condition has been moved out of it.
Demonstrate that you also state what the lifted suite does not cover, and that you keep the wiring thin on purpose so the untestable surface stays small rather than growing one hidden condition at a time.
The tradeoff is where a team spends its verification budget: a large lifted suite is cheap and fast but proves only per-record correctness, so decide deliberately how much real-machine running you buy and for which risks.
## What a job file actually contains A program handed to a distributed runtime usually mixes two kinds of code that nothing forces you to keep apart. The first is **wiring**: naming the input, declaring how records are grouped or joined, saying where the output goes, and handing the runtime the graph of steps it derives from the program before any record moves. The second is **the rule**: what one record means - which records are dropped, how a field is cleaned, how two fields become a third, which arithmetic produces the settled amount. Almost every defect a reviewer cares about lives in the second kind, and the second kind has no need of a cluster. A cluster exists to run the same rule over more records than one machine can finish; it does not change what the rule computes on one record. **The testable part of a distributed job is therefore the per-record rule, lifted out of the distributed wiring into an ordinary function.** ## The shape that makes a function callable Whether a rule can be lifted is decided entirely by its signature: - **Plain values in, plain values out.** The parameter is a record - a map, a tuple, a small class of your own - not the runtime's own collection type, not a handle, not a context object it supplies. - **No ambient reads.** The function does not read configuration, open a connection, reach into distributed file storage or object storage (the shared place every worker reads input from and writes output to), or ask the machine what time it is. Anything of that sort arrives as an argument. - **No ambient writes.** It returns its result, and returns its rejects, rather than incrementing a counter the runtime owns or logging through a runtime-supplied object. - **Deterministic for a given input**, or taking its source of variation - a seed, a timestamp - as a parameter. A function of that shape can be called from a unit test, from a script, from an interactive session, from a second program. The test needs no cluster, no credentials, no network, and a failure names the exact input that produced it rather than a worker that no longer exists. ## What is left behind, and why it should be thin | layer | what it holds | how you exercise it | |---|---|---| | the per-record rule | cleaning, filters, derivations, the record-to-key mapping | called directly from an ordinary test, thousands of cases in a second | | the wiring | input location, grouping and join declarations, output location and mode | an in-process run where the runtime offers one - the whole job executed inside the test's own process on one machine, with no network and no second worker | | the runtime | cutting the input into pieces, placing workers, moving records between them, recovering from a lost one | never yours to test; exercised only by running on real machines | The discipline that keeps the first row large is a rule about the second: **no business condition inside the wiring**. A filter written as a condition inside a declared join, a special case hidden in an output expression, a cutoff date typed into a grouping declaration - each of those can only be exercised by running the job. ## Where the lift is not mechanical 1. **Declarative surfaces.** Runtimes differ sharply here. Some take a hand-written per-record function and call it for every record, and the lift is mechanical. Others want the transform expressed in the runtime's own vocabulary of operations, in which case there is no hand-written function to lift at all, and the unit you can exercise is the expression applied to a tiny input. Say which surface you are on before claiming the lift is free. 2. **Steps that are not per-record.** Grouping, joining, and a step that carries a retained set between records are compositions, not rules. Fragments inside them lift - the mapping from record to grouping key, the merge of two partial values - while the composition itself does not. 3. **Rules needing a costly resource.** A per-record function that must consult a lookup table takes the table as a parameter. The test hands it three entries; the wiring builds the real one and arranges for each worker to have it. ## What the lift buys, and what it does not It buys fast feedback, exhaustive case coverage, and a failure message that points at an input rather than at a machine. It does not buy anything that only exists once records are redistributed between workers, once a worker is lost and its share recomputed, once one key holds most of the records, or once the function has to cross a process boundary. A candidate who lists the second half unprompted is the one who has shipped a pipeline rather than only written one.
- How do you keep a per-record rule that needs a large lookup table testable?Take the table as a parameter. The test passes a three-entry map; the wiring builds the real one and arranges for each worker to have a copy. How a runtime gets that copy to the workers is a separate subject (Sending the Small Side). The function's signature stays plain either way, which is the only property the test depends on.
- What do you do when the runtime's surface is its own expression vocabulary and there is no function to lift?The testable unit becomes the expression rather than a function. Keep each expression named, small and applied to a tiny handcrafted input, so a failure still points at one rule. Accept honestly that a larger share of the logic is only exercised by running something, and say so rather than claiming the same coverage you would get from lifted functions.
- Should the lifted function raise on a bad record or return something?Prefer returning: the record, nothing, or a reject carrying the reason. A thrown exception inside a per-record function is handled by whatever the runtime does with a failing unit of work, which differs between runtimes and can turn one bad record into a failed job. Returning rejects keeps that policy in the wiring, where you can choose it.
saying these in an interview costs you the question
- Says a distributed job simply cannot be tested without a cluster.
- Leaves the business rule in the wiring and tests it by running the whole job.
- Writes the per-record function to take the runtime's own collection type.
- Lets the function read configuration and open its own connection to storage.
- Claims a green lifted-function suite proves the job is correct on the cluster.