Which layer should I stub when testing a Remix integration without production credentials?
0 reputation · 28 Mar 2024, 01:46 UTC
0 reputation · 28 Mar 2024, 01:46 UTC
I'm building integration tests for a Remix v2 app whose loaders call a third-party API that requires credentials I don't want anywhere near CI. The goal is realistic coverage of the route components and their data flow using only synthetic data.
The Remix testing docs describe createRemixStub from @remix-run/testing, where I supply stub loaders returning canned JSON. That works, but it bypasses my real loader code entirely, including serialization, headers, and error-status handling. The alternative I keep seeing is keeping the real loaders and intercepting their outbound fetch calls at the network layer with something like MSW, plus injecting fake env vars in the test setup instead of loading production .env files.
A complicating factor: with the React Router 7 transition, the helper names and import paths differ (createRemixStub vs createRoutesStub), so whatever convention I pick now may need to migrate.
Which stubbing layer gives the best fidelity-to-maintenance tradeoff for credential-free integration tests? Does mixing both approaches in one suite cause problems? And does the choice change meaningfully if the app will move to React Router 7?
30775 reputation · 28 Mar 2024, 04:01 UTC
Keep your real loaders running and intercept their outbound fetch calls with MSW's Node server, injecting fake credentials via process.env in a shared test setup. Reserve createRemixStub from @remix-run/testing for what it does well — rendering route components against canned data — and treat it as a component-testing tool, not your integration layer. That split gives the best fidelity-to-maintenance tradeoff for the coverage you described.
Your goal is realistic coverage of route components and their data flow. Handing createRemixStub canned JSON bypasses the loader entirely, so:
Response error statuses in your real loaders go untested.With MSW handlers, the loader is real. A handler returning 401 exercises your actual error-status path through to the error boundary, and simulating 401/500 responses is trivial. Handlers are also framework-agnostic and reusable across suites.
Mixing is workable if each test file declares its target layer: stub-loader tests cover component rendering; MSW-backed tests cover loader behavior. The real risk is duplicated fixture maintenance and drift between the two data shapes. Share typed fixtures (or a schema) so stub-loader payloads and MSW handler bodies come from one source.
The counterpart of createRemixStub is createRoutesStub, exported from react-router rather than @remix-run/testing, with import paths shifting packages. MSW handlers survive untouched because loaders still execute in Node and call fetch. Verify the exact export against your installed versions before codifying conventions — these names are version-sensitive across the v2-to-v7 transition, and I'm assuming Remix v2 as currently released.
Assuming Vitest and MSW 2.x:
// test/setup.ts
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
process.env.THIRD_PARTY_API_KEY = 'test-key'; // fake, never real
export const server = setupServer(
http.get('https://api.example.com/v1/things', () =>
HttpResponse.json({ items: [{ id: 't-1' }] }),
),
);
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Three details matter:
onUnhandledRequest: 'error' is your leak guard: any request escaping the handlers fails the test, proving nothing reaches the real API in CI.msw/node, not the browser service-worker mode — loaders run in Node even under jsdom.NODE_ENV so production .env files never load under test; a stray dotenv call is the classic path for real credentials leaking into CI logs.Two one-time verifications: have a handler return 401 and assert the thrown Response reaches the error boundary with the right status; then mutate a handler payload and confirm the test fails — proving the loader, not a stub, produced the data under test.
Which HTTP client do your loaders use? This recommendation assumes global fetch (Node 18+), which MSW 2.x intercepts. If loaders reach the API through an SDK whose transport is not global fetch — axios on its Node http adapter, or direct undici clients — MSW may not see the traffic. In that case, move the stub point up one level: mock the SDK client module with vi.mock, or configure the SDK to use fetch. If you can share the client, the setup can be pinned exactly.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.