Integration Testing UI
Test composed UI with real children and network stubs — the sweet spot between shallow unit tests and full E2E.
- testing
- integration-testing
UI integration tests render a coherent slice of the app — page or feature — with real child components, router, and query client, while stubbing the network. They catch wiring bugs without browser/driver cost of full E2E.
Docs: Testing Library, MSW, React Testing Library.
Where they sit
Unit (pure functions, hooks)
→ Integration (page + providers + MSW)
→ E2E (real browser + env)
See testing pyramid.
Example: profile page
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import { ProfilePage } from './ProfilePage';
import { AppProviders } from './test-utils';
const server = setupServer(
http.get('/api/me', () =>
HttpResponse.json({ id: '1', name: 'Ada', email: 'ada@example.com' }),
),
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('shows profile and edits name', async () => {
server.use(
http.patch('/api/me', async ({ request }) => {
const body = await request.json();
return HttpResponse.json({ id: '1', ...body });
}),
);
const user = userEvent.setup();
render(
<AppProviders>
<ProfilePage />
</AppProviders>,
);
expect(await screen.findByRole('heading', { name: 'Ada' })).toBeInTheDocument();
await user.clear(screen.getByLabelText(/name/i));
await user.type(screen.getByLabelText(/name/i), 'Ada Lovelace');
await user.click(screen.getByRole('button', { name: /save/i }));
expect(await screen.findByRole('status')).toHaveTextContent(/saved/i);
});
What to include
- Real feature components
- Router with initial entries
- Form libraries as used in prod
- Query cache providers
What to stub
- HTTP (MSW)
- Heavy analytics
- Time (when needed)
- Feature flags (controlled)
Avoid mocking child presentational components — you retest mocks.
Provider test harness
export function AppProviders({ children }: { children: React.ReactNode }) {
const client = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return (
<QueryClientProvider client={client}>
<MemoryRouter>{children}</MemoryRouter>
</QueryClientProvider>
);
}
Disable retries so failures surface fast.
Assertions that matter
- Roles and labels users see
- Error and empty states
- URL changes
- Disabled/busy buttons during submit
Not: internal Redux actions, CSS class names.
Footguns
- Over-mocking so nothing real runs.
- One giant “app test” for everything.
- Leaving query retries on → slow flake.
- Not testing failure HTTP paths.
Interview out-loud answer
“Integration UI tests render a feature with real children and providers, stub network with MSW, and assert user-visible outcomes. They’re my default for pages; pure unit for logic; E2E for a few critical journeys.”
Auth wrappers
Many integration tests fail on redirects to login. Provide a test harness that seeds an authenticated session (cookie/token) or renders the feature with a stubbed auth context. Don’t mock the entire page under test to avoid the auth provider.
Extra practice
Write a minimal demo in a scratch file or the playground: one happy path, one failure path, and one boundary input. If you cannot exhibit a bug that the pattern prevents, you do not own the concept yet — re-read the primary docs linked below and tighten the example until the failure is obvious.
Notes from real codebases
Teams that succeed here keep the rules mechanical: lint where possible, CI for the rest, and a short human checklist for what automation cannot see. Document exceptions with an owner name and a removal date so “temporary” escapes do not become permanent architecture.
Related on this site
- MSW mock service worker
- Testing Library queries priority
- Mocking fetch in tests
- End-to-end testing tradeoffs
- Testing Library
Further reading
Related guides
- Accessibility Testing in CIWire axe and lint rules into CI without false confidence — what automation catches, what it misses, and a practical pipeline.
- Component Testing StorybookUse Storybook as a component workshop and optional test runner — stories as living specs, interaction tests, and a11y checks.
- Contract Testing APIsKeep frontend and backend agreements honest — schema contracts, Pact-style consumer tests, and OpenAPI-driven checks without brittle E2E.
- Coverage Metrics PitfallsWhy 100% line coverage can still ship bugs — gaming metrics, useless tests, and what coverage is actually good for.
- End-to-End Testing TradeoffsWhen E2E tests earn their keep, why they flake, and how to keep a small critical-path suite instead of a slow second frontend.