Skip to content

Commit 088dabb

Browse files
committed
03/01: add exercise texts
1 parent c189814 commit 088dabb

File tree

18 files changed

+444
-10
lines changed

18 files changed

+444
-10
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Custom matchers
2+
3+
Imagine you're building a full-stack application. Inevitably, it fetches and processes data from the server in order to render the corresponding UI. To keep it both type- and runtime-safe, you introduce a validation library to define schemas for your data types and keep them in check.
4+
5+
For example, you may have a user schema:
6+
7+
```ts filename=src/schemas.ts highlight=3-6
8+
import z from 'zod'
9+
10+
export const userSchema = z.object({
11+
id: z.string(),
12+
name: z.string(),
13+
})
14+
15+
export type User = z.infer<typeof userSchema>
16+
```
17+
18+
> Above, we are defining a simple `userSchema` containing fields `id` and `name`. Then we are inferring the user object's type using `z.infer` and exporting it as a `User` type for our application to use.
19+
20+
Then, you have a function responsible for fetching the user:
21+
22+
```ts filename=src/fetch-user.ts highlight=4
23+
import type { User } from './schemas'
24+
25+
export async function fetchUser(id: string): Promise<User> {
26+
return db.user.findFirst<User>({ where: { id } })
27+
}
28+
```
29+
30+
It is clear that the `fetchUser()` function must return an object of the `User` type, which also means that it must match the `userSchema`.
31+
32+
How would you reflect this intention in tests?
33+
34+
```ts filename=src/fetch-user.test.ts
35+
import { userSchema } from './schemas'
36+
37+
test('returns the user by id', async () => {
38+
const user = await fetchUser('abc-123')
39+
const result = userSchema.safeParse(user)
40+
41+
expect(result.error).toBeUndefined()
42+
expect(result.data).toEqual({
43+
id: 'abc-123',
44+
name: 'John Maverick',
45+
})
46+
})
47+
```
48+
49+
> You would also need to mock the `fetchUser()`'s dependency on the `db` but I am omitting this step for brevity.
50+
51+
So you fetch the user with the controlled id (`await fetchUser('abc-123')`), parse it with the schema (`userSchema.safeParse(user)`), and assert that there were no errors and the parsed object matches the expected mock user.
52+
53+
While this testing strategy works, there are two issues with it:
54+
55+
1. **It's quite verbose**. Imagine employing this strategy to verify dozens of scenarios. You are paying 3 LOC for what is, conceptually, a single assertion;
56+
1. **It's distracting**. Parsing the object and validating the parsed result are _technical details_ exclusive to the intention. It's not the intention itself. It has nothing to do with the `fetchUser()` behaviors you're testing.
57+
58+
Luckily, there are ways to redesign this approach to be more declartive and expressive by using a _custom matcher_.
59+
60+
## Your task
61+
62+
👨‍💼 In this exercise, your task will be to rewrite the existing test at <InlineFile file="src/fetch-user.test.ts" /> to use the matcher called `.toMatchSchema()`. The only thing is, this matcher _doesn't exist_. You have to create it!
63+
64+
Head to <InlineFile file="vitest.setup.ts" /> and follow the instructions to implement your custom matcher. Don't forget to include the setup file in <InlineFile file="vitest.config.ts" /> and have the tests passing once you refactor them.
65+
66+
Good luck!
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"type": "module",
3+
"name": "exercises_03.assertions_01.problem.custom-matchers",
4+
"scripts": {
5+
"test": "vitest"
6+
},
7+
"devDependencies": {
8+
"@types/node": "^22.14.1",
9+
"vite": "^6.2.6",
10+
"vitest": "^3.1.1"
11+
},
12+
"dependencies": {
13+
"zod": "^3.24.2"
14+
}
15+
}

exercises/03.assertions/01.problem.custom-matchers/pnpm-lock.yaml

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { fetchUser } from './fetch-user'
2+
import { userSchema } from './schemas'
3+
4+
test('returns the user by id', async () => {
5+
const user = await fetchUser('abc-123')
6+
7+
// 💣 Remove this object parsing.
8+
// Your custom matcher will perform the parsing now.
9+
const result = userSchema.safeParse(user)
10+
11+
// 💣 Remove both of these assertions.
12+
expect(result.error).toBeUndefined()
13+
expect(result.data).toEqual({
14+
id: 'abc-123',
15+
name: 'John Maverick',
16+
})
17+
18+
// 🐨 Add a single assertion over the `user` object
19+
// as the received value, using the `.toMatchSchema()` matcher.
20+
// Provide the `userSchema` as the expected value.
21+
// 💰 expect(this).toMatchSchema(that)
22+
})
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { User } from './schemas'
2+
3+
/**
4+
* Returns the user with the given ID.
5+
*/
6+
export async function fetchUser(id: string): Promise<User> {
7+
return {
8+
id,
9+
name: 'John Maverick',
10+
}
11+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import z from 'zod'
2+
3+
export const userSchema = z.object({
4+
id: z.string(),
5+
name: z.string(),
6+
})
7+
8+
const r = userSchema.safeParse({})
9+
10+
export type User = z.infer<typeof userSchema>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ESNext",
4+
"module": "ESNext",
5+
"useDefineForClassFields": true,
6+
"skipLibCheck": true,
7+
8+
/* Bundler mode */
9+
"moduleResolution": "bundler",
10+
"allowImportingTsExtensions": true,
11+
"isolatedModules": true,
12+
"moduleDetection": "force",
13+
"noEmit": true,
14+
"verbatimModuleSyntax": true,
15+
16+
/* Linting */
17+
"strict": true,
18+
"noUnusedLocals": false,
19+
"noUnusedParameters": false,
20+
"noFallthroughCasesInSwitch": true,
21+
"noUncheckedSideEffectImports": true
22+
}
23+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"files": [],
3+
"references": [
4+
{ "path": "./tsconfig.src.json" },
5+
{ "path": "./tsconfig.node.json" },
6+
{ "path": "./tsconfig.test.json" }
7+
]
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extends": "./tsconfig.base.json",
3+
"include": ["vitest.config.ts"],
4+
"compilerOptions": {
5+
"lib": ["ES2023"],
6+
"types": ["node"]
7+
}
8+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"extends": "./tsconfig.base.json",
3+
"include": ["src/**/*.ts"],
4+
"exclude": ["src/**/*.test.ts"]
5+
}

0 commit comments

Comments
 (0)