Skip to content

Commit 1609a18

Browse files
committed
03/04: add problem and a bit of solution
1 parent 319f1d9 commit 1609a18

File tree

12 files changed

+218
-5
lines changed

12 files changed

+218
-5
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Retryable assertions
2+
3+
When testing asynchronous code, your system often arrives at the expected state _eventually_. That is why you often express your intentions in tests as "wait for X to happen" instead of claiming "X must happen immediately".
4+
5+
In Promise-based APIs, that expectation is neatly abstracted behind the `async`/`await` keywords to keep your tests clean:
6+
7+
```ts nonumber
8+
const response = await fetch('/api/songs')
9+
await expect(response.json()).toEqual(favoriteSongs)
10+
```
11+
12+
> While fetching the list of songs takes time, that eventuality is represented as a Promise that you can `await`. This guaratees that your test will not continue until the data is fetched. Quite the same applies to reading the response body stream.
13+
14+
But not all systems are designed like that. And even the systems that _are_ designed like that may not expose you the right Promises to await.
15+
16+
In those situations, you likely reach to utilities like `waitFor()` to help you express the expected eventual state of your tested code.
17+
18+
```ts nonumber nocopy highlight=3-5
19+
api.sideEffect()
20+
21+
await vi.waitFor(() => {
22+
expect(api.state).toEqual(expectedState)
23+
})
24+
```
25+
26+
And this works great. But today, I'd like to show you a different approach to expressing eventual expectations.
27+
28+
## Your task
29+
30+
👨‍💼 In <InlineFile file="src/client.test.ts" />, a new challenge is waiting for you. Follow the instructions to replace `vi.waitFor()` with a new API called `expect.poll()`. Make sure your tests are passing once you're done, and see you soon!
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"type": "module",
3+
"name": "exercises_03.assertions_04.solution.retryable-assertions",
4+
"scripts": {
5+
"dev": "vite",
6+
"test": "vitest",
7+
"build": "vite build"
8+
},
9+
"devDependencies": {
10+
"vite": "^6.2.6",
11+
"vitest": "^3.1.1"
12+
}
13+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Client } from './client'
2+
3+
test('receives a basket of fruits', async () => {
4+
const client = new Client()
5+
const responseListener = vi.fn()
6+
client.request('fruits', responseListener)
7+
8+
// 💣 Delete this `vi.waitFor()` block.
9+
// You will replace it with something else in a moment.
10+
await vi.waitFor(() => {
11+
expect(responseListener).toHaveBeenCalledWith(['apple', 'banana', 'cherry'])
12+
})
13+
14+
// 🐨 Create a polling assertion by using `expect.poll()`.
15+
// 💰 await expect.poll(() => received).toHaveBeenCalledWith(expected)
16+
17+
// 🐨 Use the same list of fruits as the expected value for the assertion.
18+
// 💰 ['apple', 'banana', 'cherry']
19+
})
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { EventEmitter } from 'node:events'
2+
import { Server } from './server'
3+
4+
export class Client extends EventEmitter {
5+
private server: Server
6+
7+
constructor() {
8+
super()
9+
this.server = new Server()
10+
}
11+
12+
public request(
13+
requestType: string,
14+
responseListener: (data: unknown) => void,
15+
) {
16+
this.server.emit('request', requestType, responseListener)
17+
}
18+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { EventEmitter } from 'node:events'
2+
3+
export class Server extends EventEmitter {
4+
constructor() {
5+
super()
6+
this.on('request', (requestType, respondWith) => {
7+
queueMicrotask(() => {
8+
switch (requestType) {
9+
case 'fruits': {
10+
return respondWith(['apple', 'banana', 'cherry'])
11+
}
12+
13+
default: {
14+
return respondWith('unknown request')
15+
}
16+
}
17+
})
18+
})
19+
}
20+
}
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/**/*"],
4+
"exclude": ["src/**/*.test.ts*"]
5+
}
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.app.json" },
5+
{ "path": "./tsconfig.node.json" },
6+
{ "path": "./tsconfig.test.json" }
7+
]
8+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "./tsconfig.base.json",
3+
"compilerOptions": {
4+
"lib": ["ES2023"]
5+
},
6+
"include": ["vitest.config.ts"]
7+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extends": "./tsconfig.app.json",
3+
"include": ["src/**/*", "src/**/*.test.ts*"],
4+
"exclude": [],
5+
"compilerOptions": {
6+
"types": ["vitest/globals"]
7+
}
8+
}

0 commit comments

Comments
 (0)