Part II β The Shield Β· Lesson 4.2
Unit & Integration Testing
In one lineUnit tests prove one thing in isolation; integration tests prove the seams between them hold.
Where you've seen itCart pricing. The discount maths is a pure function; whether the coupon field actually updates the total is a different question entirely.
Diagram
SCOPE BOUNDARY β the only real difference between the two
UNIT INTEGRATION
βββββββββββββββββ ββββββββββββββββββββββββββββ
β calcTotal() β real β <CartPanel> β real
βββββββββββββββββ β ββ <CouponInput> β real
β² β ββ <LineItems> β real
β everything else mocked β ββ calcTotal() β real
ββββββββββββββ¬ββββββββββββββ
β
fetch / API β mocked (MSW)
router β real, in-memory
store β realTHE MOCKING DIAL β turn it as far right as you can afford
everything mocked ββββββββββββββββββββββββββββΊ nothing mocked
fast, brittle, slow, honest,
tests your mocks tests reality
β² over-mocked unit tests pass while β²
production is broken E2E lives hereMOCK AT THE NETWORK, NOT AT THE MODULE
β jest.mock('./api') couples the test to your file layout;
a wrong URL or bad payload shape passes
β MSW intercepts fetch real code path, real serialisation,
contract-shaped responsesHow it works
- Unit: pure logic β formatters, reducers, selectors, validation, date maths. No DOM, no network, microseconds each.
- Integration: render a real component tree, interact like a user, assert on visible output. Mock only the network boundary.
- Query by role and label, not by class name or test id where possible β that keeps tests behaviour-shaped and accidentally checks accessibility.
- Assert on outcomes, never on internal state or the number of times a function was called.
- One reason to fail per test; a test asserting six things reports the first failure and hides the rest.
// integration: the seam is what's under test
render(<CartPanel items={items} />);
await user.type(screen.getByLabelText('Coupon code'), 'SAVE20');
await user.click(screen.getByRole('button', { name: 'Apply' }));
expect(await screen.findByText('βΉ1,600')).toBeVisible();Trade-offs
| Unit test when | Integration test when |
|---|---|
| Logic has many branches | The bug would live in the wiring |
| The function is pure | State flows across components |
| You need 50 cases fast | You want refactor-resilience |
Coverage honesty: 100% line coverage with mocked collaborators proves your mocks agree with themselves. Track coverage as a smoke alarm for untested files, never as a target to hit.
Interview angle
"Where's the line between a unit and an integration test?"
- It's the mocking boundary, not the file count β one component with a mocked child is a unit test.
- I default to mocking only the network (MSW), so the real render, state, and event path stay under test.
- That makes tests survive refactors, which is the property that decides whether a suite lasts.
Recap
- The difference is scope and what you mock, nothing else.
- Mock at the network boundary; module mocks test your file layout.
- Assert on what the user sees, not on how the code got there.