Skip to content

Commit b401987

Browse files
committed
feat: add api tests module
1 parent 20437d1 commit b401987

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1350
-23
lines changed

demo-app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"name": "demo-app",
3+
"version": "1.0.0",
34
"type": "module",
45
"scripts": {
56
"dev": "tsx watch src/index.tsx"

demo-app/src/api/app/health.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { describeRoute } from "hono-openapi";
2+
import { PRODUCTS, ProductSchema } from "../../data/products.js";
3+
import { resolver } from "hono-openapi/zod";
4+
import { z } from "zod";
5+
6+
export const HealthSchema = z.object({
7+
status: z.union([z.literal("UP"), z.literal("DOWN")]),
8+
});
9+
10+
export type Health = z.infer<typeof HealthSchema>;
11+
12+
export function getHealthDoc() {
13+
return describeRoute({
14+
description: "Get api health",
15+
responses: {
16+
200: {
17+
description: "Successful response",
18+
content: {
19+
"application/json": {
20+
schema: resolver(z.array(HealthSchema)),
21+
},
22+
},
23+
},
24+
},
25+
});
26+
}
27+
28+
export function getHealth(): Health {
29+
return { status: "UP" };
30+
}

demo-app/src/api/app/info.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { describeRoute } from "hono-openapi";
2+
import { resolver } from "hono-openapi/zod";
3+
import { z } from "zod";
4+
import packageJson from "../../../package.json";
5+
6+
export const InfoSchema = z.object({
7+
name: z.string(),
8+
version: z.string(),
9+
});
10+
11+
export type Info = z.infer<typeof InfoSchema>;
12+
13+
export function getInfoDoc() {
14+
return describeRoute({
15+
description: "Get api info",
16+
responses: {
17+
200: {
18+
description: "Successful response",
19+
content: {
20+
"application/json": {
21+
schema: resolver(z.array(InfoSchema)),
22+
},
23+
},
24+
},
25+
},
26+
});
27+
}
28+
29+
export function getInfo(): Info {
30+
return { name: packageJson.name, version: packageJson.version };
31+
}

demo-app/src/api/app/mappings.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { describeRoute } from "hono-openapi";
2+
import { resolver } from "hono-openapi/zod";
3+
import { z } from "zod";
4+
import packageJson from "../../../package.json";
5+
import type { Hono } from "hono";
6+
7+
export const MappingsSchema = z.object({
8+
mappings: z.array(z.object({
9+
method: z.string(),
10+
path: z.string(),
11+
})),
12+
});
13+
14+
export type Mappings = z.infer<typeof MappingsSchema>;
15+
16+
export function getMappingsDoc() {
17+
return describeRoute({
18+
description: "Get api mappings",
19+
responses: {
20+
200: {
21+
description: "Successful response",
22+
content: {
23+
"application/json": {
24+
schema: resolver(z.array(MappingsSchema)),
25+
},
26+
},
27+
},
28+
},
29+
});
30+
}
31+
32+
export function getMappings(app: Hono): Mappings {
33+
return {
34+
mappings: app.routes.map((r) => ({ method: r.method, path: r.path })),
35+
};
36+
}

demo-app/src/index.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import { GUESTBOOK } from './data/guestbook.js';
1919
import { Login } from './pages/login.js';
2020
import { ProvideHonoContext } from './utils/context.js';
2121
import { AdminGuestbook } from './pages/admin-guestbook.js';
22+
import { getHealth, getHealthDoc } from './api/app/health.js';
23+
import { getInfo, getInfoDoc } from './api/app/info.js';
24+
import { getMappings, getMappingsDoc } from './api/app/mappings.js';
2225

2326
const app = new Hono();
2427

@@ -49,10 +52,13 @@ app
4952
.get('/pastry', (c) => c.html(withContexts(Pastry, c)))
5053
.get('/terms-and-conditions', (c) => c.html(withContexts(TermsAndConditions, c)))
5154
// api
55+
.get('/api/app/health', getHealthDoc(), (c) => c.json(getHealth()))
56+
.get('/api/app/info', getInfoDoc(), (c) => c.json(getInfo()))
57+
.get('/api/app/mappings', getMappingsDoc(), (c) => c.json(getMappings(app)))
5258
.get('/api/v1/discoveries', getDiscoveriesDoc(), (c) => c.json(getDiscoveries()))
5359
// swagger
5460
.get(
55-
'/doc',
61+
'/api/doc',
5662
openAPISpecs(app, {
5763
documentation: {
5864
info: {
@@ -64,9 +70,9 @@ app
6470
},
6571
})
6672
)
67-
.get('/swagger', swaggerUI({ url: '/doc' }))
68-
.get('/swagger-ui', swaggerUI({ url: '/doc' }))
69-
.get('/swagger-ui/index.html', swaggerUI({ url: '/doc' }));
73+
.get('/api/swagger', swaggerUI({ url: '/api/doc' }))
74+
.get('/api/swagger-ui', swaggerUI({ url: '/api/doc' }))
75+
.get('/api/swagger-ui/index.html', swaggerUI({ url: '/api/doc' }));
7076

7177
const port = 3000;
7278
console.log(`Server is running on http://localhost:${port}`);

demo-app/tsconfig.json

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@
55
"strict": true,
66
"verbatimModuleSyntax": true,
77
"skipLibCheck": true,
8-
"types": [
9-
"node"
10-
],
8+
"types": ["node"],
119
"jsx": "react-jsx",
1210
"jsxImportSource": "hono/jsx",
11+
"resolveJsonModule": true
1312
}
14-
}
13+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<!-- .slide: class="transition" -->
2+
3+
# Make API call
4+
5+
##==##
6+
7+
<!-- .slide: class="with-code" -->
8+
9+
# Access the page...
10+
11+
```TypeScript
12+
test('ui test', ({ page }) => {
13+
await page.goto('/');
14+
});
15+
```
16+
17+
<!-- .element: class="big-code" -->
18+
19+
Notes:
20+
21+
- as we see before
22+
23+
##==##
24+
25+
<!-- .slide: class="with-code" -->
26+
27+
# Access the API!
28+
29+
```TypeScript
30+
test('ui test', ({ page }) => {
31+
await page.goto('/');
32+
});
33+
34+
test('api test', ({ request }) => {
35+
await request.get('/api/health');
36+
});
37+
```
38+
39+
<!-- .element: class="big-code" -->
40+
41+
Notes:
42+
43+
- as simple as before!
44+
- Playwright offer the request fixture built-in
45+
- it gives method to make any api calls
46+
47+
##==##
48+
49+
<!-- .slide: class="with-code" -->
50+
51+
# Exemple 1: simulate submitting a login form
52+
53+
```TypeScript
54+
const res = await request.post('/api/login', {
55+
form: { email: '[email protected]', password: 'SuperSecure1' }
56+
});
57+
```
58+
59+
<!-- .element: class="big-code" -->
60+
61+
Notes:
62+
63+
- to speed up auth, we can use api call to simulate login
64+
- pay attention, it's not how the user will authenticate, so keep some test using the UI!
65+
66+
##==##
67+
68+
<!-- .slide: class="with-code" -->
69+
70+
# Exemple 1: simulate submitting a login form (FormData version)
71+
72+
```TypeScript
73+
const form = new FormData();
74+
form.set('email', '[email protected]')
75+
form.set('password', 'SuperSecure1')
76+
const res = await request.post('/api/login', { form });
77+
```
78+
79+
<!-- .element: class="big-code" -->
80+
81+
##==##
82+
83+
<!-- .slide: class="with-code" -->
84+
85+
# Exemple 2: make an authenticate POST call
86+
87+
```TypeScript
88+
const res = await request.post('/api/v1/secret-user-data', {
89+
headers: { 'Authorization': 'Bearer A.Pretty-Legit-Jwt.Token' }
90+
});
91+
```
92+
93+
<!-- .element: class="big-code" -->
94+
95+
##==##
96+
97+
<!-- .slide: class="with-code" -->
98+
99+
# Exemple 3: cleanup data after running a UI test
100+
101+
```TypeScript
102+
const res = await request.delete(`/api/v1/messages/${messageIdCreatedDuringTheTest}`);
103+
```
104+
105+
<!-- .element: class="big-code" -->
106+
107+
Notes:
108+
109+
- some times we want to cleanup data we created/updated during the test
110+
- using API call it can be easier to make this cleanup!
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<!-- .slide: class="transition" -->
2+
3+
# Response validation
4+
5+
##==##
6+
7+
<!-- .slide: class="with-code" -->
8+
9+
# Checking response status
10+
11+
```TypeScript
12+
const res = await request.get('/api/health');
13+
expect(res.status()).toBe(200);
14+
```
15+
16+
<!-- .element: class="big-code" -->
17+
18+
Notes:
19+
20+
- the result of the request methods call will give access to the full response
21+
- we can get the status for example and checking the value with low level assertions (the ones that does not make any retry or DOM actions)
22+
23+
##==##
24+
25+
<!-- .slide: class="with-code" -->
26+
27+
# Checking response headers
28+
29+
```TypeScript
30+
const res = await request.post('/api/login', {
31+
form: { email: '[email protected]', password: 'SuperSecure1' }
32+
});
33+
expect(body.headers()).toHaveProperty('Content-Type');
34+
expect(body.headers()['Content-Type']).toBe('application/json');
35+
```
36+
37+
<!-- .element: class="big-code" -->
38+
39+
Notes:
40+
41+
- nothing special, but you can access headers too
42+
43+
##==##
44+
45+
<!-- .slide: class="with-code" -->
46+
47+
# Checking response body
48+
49+
```TypeScript
50+
const res = await request.post('/api/login', {
51+
form: { email: '[email protected]', password: 'SuperSecure1' }
52+
});
53+
const body = await res.json();
54+
expect(body.token).not.toHaveLenght(0);
55+
```
56+
57+
<!-- .element: class="big-code" -->
58+
59+
Notes:
60+
61+
- to access the body (here as json, but as text, buffer, stream, blob, etc.) we need to await it
62+
- then you can manipulate the body as you want like you can make with a `fetch()` response
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<!-- .slide: class="transition" -->
2+
3+
# Mix between API tests and E2E Tests
4+
5+
##==##
6+
7+
<!-- .slide: class="quote-slide" -->
8+
9+
## When use API tests ? When E2E Tests ?
10+
11+
Notes:
12+
13+
- it depends on your test's strategy
14+
- most of the time: make a mix of both,
15+
- some things are easier/faster to test on API level
16+
- some things can only be tested with E2E to be realistic
17+
- recommendation: create a separate project for API testing (what will not make on the lab), it will be easier to keep tidy

docs/markdown/08-api-tests/99-lab-writing-an-api-test-suite.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@
66

77
<br>
88

9-
1. TODO
10-
2. TODO
11-
3. TODO
12-
13-
<br>
14-
15-
- TODO
9+
1. Create a new file `tests/api.spec.ts` ;
10+
2. Create a test that check `GET /api/app/health` ;
11+
3. Create a test that check `GET /api/app/info` ;
12+
4. Create a test that check `GET /api/app/mappings` ;
13+
5. Create a test that check `GET /api/v1/discoveries` ;
1614

1715
### npm run 07-api-tests

0 commit comments

Comments
 (0)