Replies: 2 comments 2 replies
|
Update: I gave 3) a go and it's working nicely so far, and with no difference in speed. Here's my prototype in case anyone else wants to try: App.test.tsx: /**
* @jest-environment jsdom
*/
import React from "react";
import { App } from "./App";
import { setupTestApi } from "./test-api";
import { createDbConnection as mockCreateDbConnection } from "src/data/db.fixture";
jest.mock("src/data/db", () => ({
createDbConnection: jest.fn(() => mockCreateDbConnection()),
}));
const testApi = setupTestApi();
let apiBaseUrl: string;
beforeAll(async () => {
apiBaseUrl = await testApi.start();
});
afterAll(() => testApi.stop());
describe('App', () => {
// ... test as usual, making sure that requests go apiBaseUrl ...
})test-api.ts: import { AddressInfo } from "net";
import express from "express";
import * as http from "http";
import { apiResolver } from "next/dist/server/api-utils/node";
import itemsRoute from "src/pages/api/items";
export const setupTestApi = (): {
start: () => Promise<string>;
stop: () => void;
} => {
const app = express();
const apiContext = {
previewModeEncryptionKey: "",
previewModeId: "",
previewModeSigningKey: "",
};
app.all("/api/items", (req, res) =>
apiResolver(req, res, req.query, itemsRoute, apiContext, false)
);
app.use((req, _res, next) => {
console.warn("Unhandled request " + req.url);
next();
});
let server: http.Server | null = null;
return {
start: async () => {
server = await new Promise<http.Server>((resolve) => {
const server = app.listen(0, () => resolve(server));
});
return (
"http://localhost:" + (server.address() as AddressInfo).port + "/api"
);
},
stop: () => {
if (!server) throw new Error("Server not started");
server.close();
},
};
};jest.setup.js: // Required to make TextEncoder accessible in jsdom environment, in turn
// required to run test server
import { TextEncoder, TextDecoder } from "util";
global.TextEncoder = TextEncoder;
global.TextDecoder = TextDecoder;There's still a risk that tests and actual API get out of sync when routes are added or renamed, but that's far easier to catch than mismatches in implementation. Also, I don't have routes of the form |
|
Hey, @bard. That's a great question! In any strategy you chose, I'd be careful in trusting backend as the source of truth. I'd rather encourage to have a strictly typed contract between backend and frontend, like OpenAPI spec or generated types from a GraphQL schema, just as you've mentioned. The reason for that is that implementations may be faulty, and taking backend runtime as a source of truth opens up for a brand new list of problems. The worst part is that reasoning about a fixed state of the system (frontend in this case) becomes extremely hard. This is where I like explicit mocks because they depict API state at a fixed point of time. But I understand that in larger projects it may be unsustainable to maintain manually written mocks that will inevitably get out of sync from whichever runtime your application is actually hitting in production. If I had to tackle this issue, I'd go for the static spec route as my first choice. If, for some reason, I can't, I'd do the following:
This would make that network snapshot a source of truth fixed in time, which is always what I want. I've been working on an in-house solution to generate handlers out of HAR files, and I dare say it's in a good shape right now but it's not open sourced and, frankly, I don't know if it ever be. Given my recent burnout, I don't see how throwing a new tool into the furnace would make my life any more manageable. Perhaps this solution will surface as a paid library in time. I honestly don't know. So, for now, you can use https://github.com/Tapico/tapico-msw-webarchive to generate handlers from your HAR files. |
Uh oh!
There was an error while loading. Please reload this page.
This is more of a high-level strategy question.
Context: a NextJS project based containing API routes and the frontend relying on them. (For simplicity, let's assume that said routes are called from the client side only, no SSR.)
Scenario:
src/pages/api/items.tsand notifies frontend dev Bobsrc/mocks/msw-mocks.ts, editssrc/pages/items.tsxuntil tests pass, commits and push/itemspage, only to be greeted by a white page and a stack trace in the console.What happened?
At step 2), Bob's changes weren't so equivalent after all. Whether due to wrong understanding or wrong implementation, it doesn't matter, what matters is that correctness of the system as a whole ultimately depended on someone eyeballing two pieces of code and deciding whether one was representative of the other. No automated tests verified consistency across the wire (the tests at 2) ran against the mocks).
(Note that this still a relatively optimistic scenario: at 1), Alice might have forgotten to notify Bob, yet tests would still not have broken.)
I'd be interested in hearing your strategies to deal with this fairly common scenario. Ideas that come to mind:
msw/node.I find 3) attractive because you can quickly stub new routes just like you do with
msw, but the stubs evolve into actual routes that don't run the risk of getting out of sync with mocks (because there wouldn't be any). On the flip side, running a server for each test file might (haven't tried yet) slow things down, and slow tests are run less often.Thoughts?
All reactions