Skip to content

Commit efbc449

Browse files
authored
Add docs for testing (#1258)
1 parent 1877f90 commit efbc449

File tree

1 file changed

+142
-1
lines changed

1 file changed

+142
-1
lines changed

docs/src/content/docs/advanced.md

+142-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ title: Advanced
33
description: Advanced usage as well as tips, tricks, and best practices
44
---
55

6-
Various anselary topics and advanced usage.
6+
Advanced usage and various topics.
77

88
## Data fetching
99

@@ -16,6 +16,147 @@ Fetching data can be done simply and safely using an **automatically-typed fetch
1616
>
1717
> A good fetch wrapper should **never use generics.** Generics require more typing and can hide errors!
1818
19+
## Testing
20+
21+
One of the most common causes of false positive tests is when mocks are out-of-date with the actual API responses.
22+
23+
`openapi-typescript` offers a fantastic way to guard against this with minimal effort. Here’s one example how you could write your own helper function to typecheck all mocks to match your OpenAPI schema (we’ll use [vitest](https://vitest.dev/)/[vitest-fetch-mock](https://www.npmjs.com/package/vitest-fetch-mock) but the same principle could work for any setup):
24+
25+
Let’s say we want to write our mocks in the following object structure, so we can mock multiple endpoints at once:
26+
27+
```
28+
{
29+
[pathname]: {
30+
[HTTP method]: {
31+
[HTTP status code]: { [some mock data] }
32+
}
33+
}
34+
}
35+
```
36+
37+
Using our generated types we can then infer **the correct data shape** for any given path + HTTP method + status code. An example test would look like this:
38+
39+
```ts
40+
// my-test.test.ts
41+
import { mockResponses } from "../test/utils";
42+
43+
describe("My API test", () => {
44+
it("mocks correctly", async () => {
45+
mockResponses({
46+
"/users/{user_id}": {
47+
get: {
48+
200: { id: "user-id", name: "User Name" }, // ✅ Correct 200 response
49+
404: { code: "404", message: "User not found" }, // ✅ Correct 404 response
50+
},
51+
},
52+
"/users": {
53+
put: {
54+
201: { status: "success" }, // ✅ Correct 201 response
55+
},
56+
},
57+
});
58+
59+
// test 1: GET /users/{user_id}: 200 returned by default
60+
await fetch("/users/user-123");
61+
62+
// test 2: GET /users/{user_id}: 404 returned if `x-test-status` header sent
63+
await fetch("/users/user-123", { headers: { "x-test-status": 404 } });
64+
65+
// test 3: PUT /users: 200
66+
await fetch("/users", {
67+
method: "PUT",
68+
body: JSON.stringify({ id: "new-user", name: "New User" }),
69+
});
70+
71+
// test cleanup
72+
fetchMock.resetMocks();
73+
});
74+
});
75+
```
76+
77+
_Note: this example uses a vanilla `fetch()` function, but any fetch wrapper—including [openapi-fetch](/openapi-fetch)—could be dropped in instead without any changes._
78+
79+
And the magic that produces this would live in a `test/utils.ts` file that can be copy + pasted where desired (hidden for simplicity):
80+
81+
<details>
82+
<summary>📄 <strong>test/utils.ts</strong></summary>
83+
84+
```ts
85+
// test/utils.ts
86+
import { paths } from "./api/v1/my-schema"; // generated by openapi-typescript
87+
88+
// Settings
89+
// ⚠️ Important: change this! This prefixes all URLs
90+
const BASE_URL = "https://myapi.com/v1";
91+
// End Settings
92+
93+
// type helpers — ignore these; these just make TS lookups better
94+
type FilterKeys<Obj, Matchers> = { [K in keyof Obj]: K extends Matchers ? Obj[K] : never }[keyof Obj];
95+
type PathResponses<T> = T extends { responses: any } ? T["responses"] : unknown;
96+
type OperationContent<T> = T extends { content: any } ? T["content"] : unknown;
97+
type MediaType = `${string}/${string}`;
98+
99+
/**
100+
* Mock fetch() calls and type against OpenAPI schema
101+
*/
102+
export function mockResponses(responses: {
103+
[Path in keyof Partial<paths>]: {
104+
[Method in keyof Partial<paths[Path]>]: {
105+
[Status in keyof Partial<PathResponses<paths[Path][Method]>>]: FilterKeys<OperationContent<PathResponses<paths[Path][Method]>[Status]>, MediaType>;
106+
};
107+
};
108+
}) {
109+
fetchMock.mockResponse((req) => {
110+
const mockedPath = findPath(req.url.replace(BASE_URL, ""), Object.keys(responses))!;
111+
// note: we get lazy with the types here, because the inference is bad anyway and this has a `void` return signature. The important bit is the parameter signature.
112+
if (!mockedPath || (!responses as any)[mockedPath]) throw new Error(`No mocked response for ${req.url}`); // throw error if response not mocked (remove or modify if you’d like different behavior)
113+
const method = req.method.toLowerCase();
114+
if (!(responses as any)[mockedPath][method]) throw new Error(`${req.method} called but not mocked on ${mockedPath}`); // likewise throw error if other parts of response aren’t mocked
115+
const desiredStatus = req.headers.get("x-status-code");
116+
const body = (responses as any)[mockedPath][method];
117+
return {
118+
status: desiredStatus ? parseInt(desiredStatus, 10) : 200,
119+
body: JSON.stringify((desiredStatus && body[desiredStatus]) ?? body[200]),
120+
};
121+
});
122+
}
123+
124+
// helper function that matches a realistic URL (/users/123) to an OpenAPI path (/users/{user_id}
125+
export function findPath(actual: string, testPaths: string[]): string | undefined {
126+
const url = new URL(actual, actual.startsWith("http") ? undefined : "http://testapi.com");
127+
const actualParts = url.pathname.split("/");
128+
for (const p of testPaths) {
129+
let matched = true;
130+
const testParts = p.split("/");
131+
if (actualParts.length !== testParts.length) continue; // automatically not a match if lengths differ
132+
for (let i = 0; i < testParts.length; i++) {
133+
if (testParts[i]!.startsWith("{")) continue; // path params ({user_id}) always count as a match
134+
if (actualParts[i] !== testParts[i]) {
135+
matched = false;
136+
break;
137+
}
138+
}
139+
if (matched) return p;
140+
}
141+
}
142+
```
143+
144+
> **Additional Explanation** That code is quite above is quite a doozy! For the most part, it’s a lot of implementation detail you can ignore. The `mockResponses(…)` function signature is where all the important magic happens—you’ll notice a direct link between this structure and our design. From there, the rest of the code is just making the runtime work as expected.
145+
146+
```ts
147+
export function mockResponses(responses: {
148+
[Path in keyof Partial<paths>]: {
149+
[Method in keyof Partial<paths[Path]>]: {
150+
[Status in keyof Partial<PathResponses<paths[Path][Method]>>]: FilterKeys<OperationContent<PathResponses<paths[Path][Method]>[Status]>, MediaType>;
151+
};
152+
};
153+
});
154+
```
155+
156+
</details>
157+
158+
Now, whenever your schema updates, **all your mock data will be typechecked correctly** 🎉. This is a huge step in ensuring resilient, accurate tests.
159+
19160
## Tips
20161

21162
In no particular order, here are a few best practices to make life easier when working with OpenAPI-derived types.

0 commit comments

Comments
 (0)