Skip to content

Commit 98fdbd5

Browse files
committed
02/01: add exercise texts
1 parent 30fd705 commit 98fdbd5

File tree

16 files changed

+404
-20
lines changed

16 files changed

+404
-20
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Custom fixtures
2+
3+
Crafting custom, domain-specific utilities is a great way to elevate your test setup and turn your tests into a _composition_ of declarative steps and assertions that follow them. As always, there are multiple ways to approach test utilities. You can just import them when you need them _or_ you can use _fixtures_ instead.
4+
5+
Vitest's concept of a fixture is heavily inspired by Playwright, where you can access both built-in and custom fixtures from the test context:
6+
7+
```ts filename=playwright.test.ts highlight=3
8+
import { test } from '@playwright/test'
9+
10+
test('...', ({ page, myCustomFixture }) => {})
11+
// 👆 👆
12+
```
13+
14+
> The first argument to the test callback function is referred to as **test context**.
15+
16+
Vitest already exposes a few built-in utilities in its test context, such as the `expect` function:
17+
18+
```ts filename=vitest.test.ts highlight=1
19+
test('...', ({ expect }) => {
20+
expect(this).toBe(that)
21+
})
22+
```
23+
24+
But you can push this further. You can extend the default test context and imbue it with custom fixtures specific to the software you're testing. It's time you learned how.
25+
26+
## Your task
27+
28+
Our e-commerce application has a neat utility to calculate any given cart's total using a function called `getTotalPrice`:
29+
30+
```ts filename=src/cart-utils.ts
31+
export function getTotalPrice(cart: Cart): number {
32+
return cart.reduce((total, item) => {
33+
return total + item.price * item.quantity
34+
}, 0)
35+
}
36+
```
37+
38+
It expects a `cart: Cart` object as the argument, which means that in order to test different calculation scenarios you have to create different states of the cart. You certainly can do that manually in each test or you can utilize a custom fixture to help you out.
39+
40+
👨‍💼 In this one, your task is to _extend the default test context in Vitest_ and implement a custom fixture called `createMockCart()`. You will use `@faker-js/faker` to help you with generating random values for your mock, such as cart item names and quantity. You will implement your custom fixture so it has a default random state but also supports overriding that state on the individual test basis to model the scenarios you need.
41+
42+
🐨 Start by installing `@faker-js/faker` as a dependency:
43+
44+
```
45+
npm install @faker-js/faker --save-dev
46+
```
47+
48+
🐨 Next, proceed to <InlineFile file="test-extend.ts" /> file ane follow the instructions to implement a custom fixture.
49+
50+
🐨 Once the fixture is ready, import the custom `test` function you've created in the <InlineFile file="src/cart-utils.test.ts" /> test file _and complete it_. By the end, run `npm test` to verify that your tests are passing.
51+
52+
Good luck!
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"type": "module",
3+
"name": "exercises_02.context_01.problem.custom-fixtures",
4+
"scripts": {
5+
"test": "vitest"
6+
},
7+
"devDependencies": {
8+
"vitest": "^3.1.1"
9+
}
10+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// 🐨 Import the custom `test` function from `../test-extend`.
2+
// 💰 import { this } from 'that'
3+
import { getTotalPrice } from './cart-utils'
4+
5+
test('returns the total price for the cart', () => {
6+
// 💰 ({ createMockCart }) => {} // Inside that object, access your newly created fixture `createMockCart`. // 🐨 Destructure the first argument of the `test()` function.
7+
// 🐨 Declare a new variable called `cart` and assign it the result
8+
// of calling the `createMockCart()` fixture.
9+
// 💰 const cart = yourFixture(args)
10+
//
11+
// 🐨 Provide a fixed set of cart items to your mock cart.
12+
// This way, the state of the cart item is fixed to this test case,
13+
// guaranteeing explicit and reliable test results.
14+
// 💰 [{ price: 5, quantity: 10 }, { price: 8, quantity: 4 }]
15+
//
16+
// 🐨 Finally, write an assertion that calling `getTotalPrice()` with your
17+
// mock `cart` as an argument returns the correct total price.
18+
// 💰 expect(actual).toBe(expected)
19+
})
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export type Cart = Array<CartItem>
2+
3+
export interface CartItem {
4+
id: string
5+
name: string
6+
price: number
7+
quantity: number
8+
}
9+
10+
export function getTotalPrice(cart: Cart): number {
11+
return cart.reduce((total, item) => {
12+
return total + item.price * item.quantity
13+
}, 0)
14+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// 🐨 Import the `test` function from `vitest`.
2+
// Since you're going to create a custom version of that function,
3+
// alias its import as `testBase`.
4+
// 💰 import { this as that } from 'pkg'
5+
//
6+
// 🐨 Import the `faker` object from `@faker-js/faker`.
7+
// You will use this object to generate random data for your mock cart.
8+
//
9+
// 🐨 Lastly, import types `Cart` and `CartItem` from `./src/cart-utils`.
10+
// These will help you keep your custom fixture type-safe.
11+
//
12+
// 🐨 Declare a new TypeScript interface called `Fixtures`.
13+
// This interface will describe the types of your custom fixtures.
14+
// 💰 interface Fixtures {}
15+
//
16+
// 🐨 In the `Fixtures` interface, declare a property called `createMockCart`.
17+
// Assign an arrow function as the value of that property.
18+
// The function accepts a single argument called `items` of type `Array<Partial<CartItem>>`
19+
// and returns a `Cart` object.
20+
// 💰 fn: (arg: T) => R
21+
//
22+
// 🐨 Now, create a new variable called `test` and export it from this module.
23+
// As the value, assign it the result of calling `testBase.extend({})`.
24+
// Narrow down the types of custom fixtures by providing the `Fixtures` interface
25+
// as the type argument to the `testBase.extend()` method.
26+
// 💰 export const test = testBase.extend({})
27+
// 💰 testBase.extend<T>({})
28+
//
29+
// 🐨 In the object argument to `testBase.extend()`, declare a new async function
30+
// called `createMockCart()`. It will accept two arguments:
31+
// - Text context (an empty object);
32+
// - The `use` function to apply custom fixtures.
33+
// 💰 async createMockCart({}, use ) {}
34+
//
35+
// 🐨 In the `createMockCart` function body, await the call to the `use()` function.
36+
// As the argument to `use()`, pass an arrow function that accepts a single argument called `items`.
37+
// 💰 await use((items) => {})
38+
//
39+
// At this point, you can see TypeScript complaining about your `use()` function
40+
// as it's expected to return a `Cart` object!
41+
// 🐨 Return the result of iterating over the given `items` array.
42+
// 💰 return items.map((item) => {})
43+
//
44+
// 🐨 In the return of the `.map()`, return an object matching the `Cart` type.
45+
// Use various methods on the `faker` object to create a cart object populated with random data.
46+
// 💰
47+
// {
48+
// id: faker.string.ulid(),
49+
// name: faker.commerce.productName(),
50+
// price: faker.number.int({ min: 1, max: 25 }),
51+
// quantity: faker.number.int({ min: 1, max: 10 }),
52+
// }
53+
//
54+
// 🐨 Finally, at the end of the returned `Cart` object, spread the `item` object
55+
// from the `.map()` argument to enable value overrides from tests.
56+
// 💰 return { id, name, price, quantity, ...item }
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "./tsconfig.base.json",
3+
"include": ["src/**/*"],
4+
"exclude": ["src/**/*.test.ts*"],
5+
"compilerOptions": {
6+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
7+
"jsx": "react-jsx"
8+
}
9+
}
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": ["test-extend.ts", "src/**/*", "src/**/*.test.ts*"],
4+
"exclude": [],
5+
"compilerOptions": {
6+
"types": ["vitest/globals"]
7+
}
8+
}

0 commit comments

Comments
 (0)