FSD Frontend System Design

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.

  • Time16 min
  • DiagramScope boundary: what's real vs what's mocked
  • Chapter4 Β· Testing

Diagram

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        ← real
diagram
THE 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 here
diagram
MOCK 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 responses

How it works

  1. Unit: pure logic β€” formatters, reducers, selectors, validation, date maths. No DOM, no network, microseconds each.
  2. Integration: render a real component tree, interact like a user, assert on visible output. Mock only the network boundary.
  3. Query by role and label, not by class name or test id where possible β€” that keeps tests behaviour-shaped and accidentally checks accessibility.
  4. Assert on outcomes, never on internal state or the number of times a function was called.
  5. One reason to fail per test; a test asserting six things reports the first failure and hides the rest.
js
// 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 whenIntegration test when
Logic has many branchesThe bug would live in the wiring
The function is pureState flows across components
You need 50 cases fastYou 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.