Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ __screenshots__
test-profiles
coverage
.vitest-reports
*.DS_Store

# in a real app you'd want to not commit the .env
# file as well, but since this is for a workshop
Expand Down
3 changes: 2 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"neotan.vscode-auto-restart-typescript-eslint-servers"
"neotan.vscode-auto-restart-typescript-eslint-servers",
"qwtel.sqlite-viewer"
]
}
57 changes: 57 additions & 0 deletions epicshop/generate-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import fs from 'node:fs'

const TEST_COUNT = 500

/**
* @param {URL} exerciseDirectory
* @param {number} count
*/
async function generateTests(exerciseDirectory, count) {
const TESTS_DIR = new URL('./tests/', exerciseDirectory)

const existingTests = fs.globSync('*.test.ts', {
cwd: TESTS_DIR.pathname,
})
await Promise.all(
existingTests.map((filename) => {
return fs.promises.rm(new URL(filename, TESTS_DIR))
}),
)

await Promise.all(
Array.from({ length: count }).map((_, index) => {
const filename = `./test-${index.toString().padStart(count.toString().length, '0')}.test.ts`
const contents = `\
test('equals to ${index}', ({ expect }) => {
expect(${index}).toBe(${index})
})
`

return fs.promises.writeFile(
new URL(filename, TESTS_DIR),
contents,
'utf8',
)
}),
)
}

Promise.all([
generateTests(
new URL(
'../exercises/04.performance/03.problem.test-isolation/',
import.meta.url,
),
TEST_COUNT,
),
generateTests(
new URL(
'../exercises/04.performance/03.solution.test-isolation/',
import.meta.url,
),
TEST_COUNT,
),
]).catch((error) => {
console.error('❌ Failed to generate tests.\n\n', error)
process.exit(1)
})
37 changes: 26 additions & 11 deletions epicshop/setup.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { spawnSync } from 'child_process'
import { spawnSync } from 'node:child_process'

const styles = {
// got these from playing around with what I found from:
Expand Down Expand Up @@ -34,18 +34,33 @@ if (major < 8 || (major === 8 && minor < 16)) {
throw new Error('npm version is out of date')
}

const command =
'npx --yes "https://gist.github.com/kentcdodds/bb452ffe53a5caa3600197e1d8005733" -q'
console.log(
color('subtitle', ' Running the following command: ' + command),
)
// Generate test suites for exercises.
{
const result = spawnSync('node ./generate-tests.js', {
cwd: new URL('.', import.meta.url),
stdio: 'inherit',
shell: true,
})

const result = spawnSync(command, { stdio: 'inherit', shell: true })
if (result.status !== 0) {
process.exit(result.status)
}
}

{
const command =
'npx --yes "https://gist.github.com/kentcdodds/bb452ffe53a5caa3600197e1d8005733" -q'
console.log(
color('subtitle', ' Running the following command: ' + command),
)

const result = spawnSync(command, { stdio: 'inherit', shell: true })

if (result.status === 0) {
console.log(color('success', '✅ Workshop setup complete...'))
} else {
process.exit(result.status)
if (result.status === 0) {
console.log(color('success', '✅ Workshop setup complete!'))
} else {
process.exit(result.status)
}
}

/*
Expand Down
5 changes: 3 additions & 2 deletions exercises/02.context/02.problem.automatic-fixtures/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ Your custom fixtures can introduce side effects that run before and after the fi
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { test as testBase } from 'vitest'

interface Fixtures {
createMockFile: (content: string, filename: string) => Promise<void>
createMockFile: (content: string, filename: string) => Promise<string>
}

test.extend({
const test = testBase.extend<Fixtures>({
async createMockFile({}, use) {
// 1. Prepare.
const temporaryDirectory = await fs.promises.mkdtemp(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ In Promise-based APIs, that expectation is neatly abstracted behind the `async`/

```ts nonumber
const response = await fetch('/api/songs')
await expect(response.json()).toEqual(favoriteSongs)
await expect(response.json()).resolves.toEqual(favoriteSongs)
```

> 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.
Expand Down
3 changes: 3 additions & 0 deletions exercises/04.performance/03.problem.test-isolation/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Ignore the test files for this exercise because
# they are generated during the setup step.
/tests/**/*.test.ts
3 changes: 3 additions & 0 deletions exercises/04.performance/03.problem.test-isolation/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Test isolation

...
10 changes: 10 additions & 0 deletions exercises/04.performance/03.problem.test-isolation/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"type": "module",
"name": "exercises_04.performance_03.problem.test-isolation",
"scripts": {
"test": "vitest"
},
"devDependencies": {
"vitest": "^3.1.1"
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"lib": ["ES2023"]
},
"include": ["vitest.config.ts", "generate-tests.ts"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
globals: true,
// 🐨 Set the `isolate` property to `false`.
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Ignore the test files for this exercise because
# they are generated during the setup step.
/tests/**/*.test.ts
35 changes: 35 additions & 0 deletions exercises/04.performance/03.solution.test-isolation/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Test isolation

## TODO

- You can spot the cost of spawning the worker as a part of the `prepare` metric in the test summary.

---

By default, Vitest runs each test file in an isolated environment (a worker, a forked process, or a VM context). That is a sensible default to make sure that no state gets leaked between test files and also make it a bit easier for you to orchestrate test setup as you don't have to worry about the entire test suite at once, just a single test file at a time.

That being said, test isolation comes with a cost as spawning workers and processes takes time. Depending on the size of your test suite, that time can range from unsubstantial to quite drastic. That is why sometimes disabling test isolation can speed up your test run.

<callout-warning>I am obligated to remind you that, much like concurrency, disabling test isolation is _never a solution to a problem_. It's a change in how your testing framework runs, and it often requires you to adjust your tests in order to benefit from it.</callout-warning>

## When to disable test isolation?

As with concurrency that we've covered previously, turning off test isolation is a conscious choice that comes with a price. It is worth mentioning that when I talk about disabling isolation, I only refer to _changing how Vitest runs your test files_. In other words, it's about telling Vitest _not_ to spend time dealing with workers/forks/vms when running tests.

**When you disable test isolation, you're telling Vitest, "_It's alright, I've got the isolation covered myself_."** Isolation as a characteristic of your test suite is _mandatory_ for reliable and trustworthy testing. That is precisely why Vitest comes with isolation built-in—to help you write self-contained, independent tests. You don't need Vitest's test isolation to do that, it just makes it simpler.

<callout-info>Unfortunately, there is no way of knowing whether it's the default test isolation from Vitest that is causing a performance degradation. Your best bet is to try disabling it on a subset of tests (using workspaces) and monitor the outcome.</callout-info>

Isolation is the overarching theme when it comes to test performance. Reliable test runs cannot be achieved without properly isolated tests. This puts a similar list of criteria for disabling test isolation as we had for enabling concurrency:

- Self-contained, independent tests;
- Limited or properly managed side effects;
- Carefully arranged test setup.

This also means that you can benefit from correctly written tests in more ways than one. You can make them concurrent _and_ stop paying extra time for built-in isolation in Vitest since you've got it covered already.

> I firmly believe that while right decisions are hard, one right decision leads to two right decisions, and so on.
## Your task

👨‍💼 ...
10 changes: 10 additions & 0 deletions exercises/04.performance/03.solution.test-isolation/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"type": "module",
"name": "exercises_04.performance_03.solution.test-isolation",
"scripts": {
"test": "vitest"
},
"devDependencies": {
"vitest": "^3.1.1"
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"lib": ["ES2023"]
},
"include": ["vitest.config.ts", "generate-tests.ts"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
globals: true,
isolate: false,
},
})
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"type": "module",
"name": "exercises_04.performance_03.problem.sharding",
"name": "exercises_04.performance_04.problem.sharding",
"scripts": {
"test": "vitest"
},
Expand Down
23 changes: 23 additions & 0 deletions exercises/04.performance/04.problem.sharding/tsconfig.base.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"useDefineForClassFields": true,
"skipLibCheck": true,

/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"verbatimModuleSyntax": true,

/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
}
}
7 changes: 7 additions & 0 deletions exercises/04.performance/04.problem.sharding/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.node.json" },
{ "path": "./tsconfig.test.json" }
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.base.json",
"include": ["src/**/*", "src/**/*.test.ts*"],
"exclude": [],
"compilerOptions": {
"types": ["vitest/globals"]
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"type": "module",
"name": "exercises_04.performance_03.solution.sharding",
"name": "exercises_04.performance_04.solution.sharding",
"scripts": {
"test": "vitest"
},
Expand Down
23 changes: 23 additions & 0 deletions exercises/04.performance/04.solution.sharding/tsconfig.base.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"useDefineForClassFields": true,
"skipLibCheck": true,

/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"verbatimModuleSyntax": true,

/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
}
}
7 changes: 7 additions & 0 deletions exercises/04.performance/04.solution.sharding/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.node.json" },
{ "path": "./tsconfig.test.json" }
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.base.json",
"include": ["src/**/*", "src/**/*.test.ts*"],
"exclude": [],
"compilerOptions": {
"types": ["vitest/globals"]
}
}
Loading
Loading