An exit point is a publicly noticeable result of a unit of work: a useful return value, a state change, or a call to an external dependency. It is what a test can inspect after triggering an entry point. Internal behavior may occur along the way, but it is not an exit point unless it becomes visible to the caller or outside system. section 1.3 section 1.4
Listing 1.2 A unit of work with entry points and exit points
let total = 0;
const totalSoFar = () => {
return total;
};
const sum = (numbers) => {
const [a, b] = numbers.split(',');
const result = parseInt(a) + parseInt(b);
total += result; #1
return result;
};
#1 New functionality: calculating a running total
A call to sum has two observable results: it returns the calculated value and changes the running total. Those are separate exit points, even though they arise from one invocation. section 1.3
Choose a test technique based on the kind of result being checked:
- For a return value, invoke the entry point and assert on the value returned. section 1.5
- For a state change, invoke the entry point, then use a public sibling function or property to observe the resulting state rather than private state. section 1.5 section 8.3.1
- For a call to an uncontrolled external system, replace the outgoing dependency with a mock and verify the expected call. section 1.5 section 3.1
A unit test invokes a unit of work through an entry point and checks one specific exit point. A unit of work can be one function or span multiple functions, modules, or components; its scope runs from the triggered entry point to the noticeable result. section 1.2 section 1.9
Split callback and return-value checks
A function that adds two values and invokes a callback has two concerns. Put the callback assertion in one test and the returned-sum assertion in another. Focused tests are easier to name, and a failed assertion cannot prevent a later concern from being checked. section 7.4.4
Do not verify an internal helper merely because a mock can observe it. That couples a test to implementation details and makes it fragile when internals change. Prefer checking the public return value, observable state, or a genuine external interaction. When possible, favor return-value and state-based tests; mock-based interaction tests add maintenance cost and are best reserved for necessary outgoing dependencies. section 5.6.3 section 8.3.1 section 1.5
Listing 7.7 Checking the two exit points in separate tests
describe("trigger", () => {
it("triggers a given callback", () => {
const callback = jest.fn();
trigger(1, 2, callback);
expect(callback).toHaveBeenCalledWith("I'm triggered");
});
it("sums up given values", () => {
const result = trigger(1, 2, jest.fn());
expect(result).toBe(3);
});
});
The two tests each name and verify one observable result: triggering the callback or summing the values. This keeps requirements and failures focused. section 7.4.4
