Skip to content

Commit de0f934

Browse files
committed
migrate away from <CodeFile/>
1 parent b0940c1 commit de0f934

File tree

21 files changed

+3407
-199
lines changed

21 files changed

+3407
-199
lines changed

exercises/01.principles/01.problem.intentions/README.mdx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44

55
You have a `greet` function that should greet the user by their name. That's the _intention_ behind it. And here's its implementation:
66

7-
<CodeFile file="greet.ts" nocopy />
7+
```ts filename=greet.js nocopy nonumber
8+
function greet(name: string) {
9+
return `Hello, ${name}!`
10+
}
11+
```
812

913
👨‍💼 Now your job is to add an automated test for the <code>greet</code> function
1014
(you can put it in the same <code>greet.ts</code> file) so you can run <code>npx tsx
@@ -13,7 +17,7 @@ intended.
1317

1418
As a reminder, this is what any automated test comes down to:
1519

16-
```js
20+
```ts
1721
// Run the code and get the *actual* result.
1822
let result = code(...args)
1923

exercises/01.principles/01.solution.intentions/README.mdx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@
44

55
To get started, I will call the `greet` function with the name `"John"` and store the result in a new variable called `message`.
66

7-
<CodeFile file="greet.ts" range="5" />
7+
```ts filename=greet.ts nonumber
8+
let message = greet('John')
9+
```
810

911
With this argument, I expect the `greet` function to return a `"Hello, John!"` string. I will compare the actual `message` it returns with the expected (intended) message, and if they don't match, throw an error that lets me know something is off with the function.
1012

11-
<CodeFile file="greet.ts" range="7-9" highlight="8" />
13+
```ts filename=greet.ts highlight=2-4 nonumber
14+
if (message !== 'Hello, John!') {
15+
throw new Error(
16+
`Expected message to equal to "Hello, John!" but got "${message}"`,
17+
)
18+
}
19+
```
1220

1321
:tada: Congratulations! You've just written the most basic automated test.
1422

exercises/01.principles/02.problem.implementation-details/README.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
99
Let's run our `greet.ts` file now and see what happens.
1010

11-
```sh
11+
```sh nonumber
1212
npx tsx greet.ts
1313
```
1414

exercises/01.principles/02.solution.implementation-details/README.mdx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@ The first thing I do whenever a test fails, I look at the _assertions_ (comparis
66

77
In the case of our `greet` function, when given the name `"John"` I expect it to return the `"Hello, John!"` string.
88

9-
<CodeFile file="greet.ts" range="5-9" highlight="7" />
9+
```ts filename=greet.ts highlight=3 nonumber
10+
let message = greet('John')
11+
12+
if (message !== 'Hello, John!') {
13+
throw new Error(
14+
`Expected message to equal to "Hello, John!" but got "${message}"`,
15+
)
16+
}
17+
```
1018

1119
This is a crucial piece of information that lets me know what's the _intention_ here.
1220

exercises/02.test-structure/01.problem.assertions/README.mdx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,21 @@ We don't have any special setup to test the `greet` function. The setup phase is
1616

1717
Our action for this test is to call the `greet` function with the `'John'` string as the argument.
1818

19-
<CodeFile file="greet.ts" range="9" nocopy />
19+
```ts filename=greet.ts nocopy nonumber
20+
let message = greet('John')
21+
```
2022

2123
### Assertion
2224

2325
And our assertion is a simple `if` statement that compares the actual `message` with the message we expect to be returned (`'Hello, John!'`).
2426

25-
<CodeFile file="greet.ts" range="11-15" nocopy />
27+
```ts filename=greet.ts nocopy nonumber
28+
if (message !== 'Hello, John!') {
29+
throw new Error(
30+
`Expected message to equal to "Hello, John!" but got "${message}"`,
31+
)
32+
}
33+
```
2634

2735
## The problem
2836

exercises/02.test-structure/01.solution.assertions/README.mdx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,24 @@
44

55
First, I will move the existing `if` logic for assertions into the new `toBe` function returned from the `expect`.
66

7-
<CodeFile file="greet.ts" range="12-20" highlight="15-17" />
7+
```ts filename=greet.ts highlight=3-7 nonumber
8+
function expect(actual: unknown) {
9+
return {
10+
toBe(expected: unknown) {
11+
if (actual !== expected) {
12+
throw new Error(`Expected ${actual} to equal to ${expected}`)
13+
}
14+
},
15+
}
16+
}
17+
```
818

919
Then, refactor the existing tests to use the new `expect` function.
1020

11-
<CodeFile file="greet.ts" range="9-10" />
21+
```ts filename=greet.ts nonumber
22+
expect(greet('John')).toBe('Hello, John!')
23+
expect(congratulate('Sarah')).toBe('Congrats, Sarah!')
24+
```
1225

1326
Notice how much more human-friendly those assertions have become! Although a test is code that verifies another code, we still write them for ourselves and for our colleagues. We still write tests for humans. Preferring a more declarative style while doing so, such as our `expect` function, is one way to make sure those humans can get around faster and tackle failing tests more efficiently.
1427

exercises/02.test-structure/02.solution.test-blocks/README.mdx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,29 @@
44

55
I start with defining the `title` and `callback` arguments on the `test()` function. Those will represent the _test's title_ and the _test itself_, respectively. Next, I invoke the `callback()` function, which will run whichever test we provide. Since failed assertions throw errors, I wrap the `callback()` in a `try/catch` block to prevent the process from exiting on failed assertions and also to print those nicely in the terminal's output.
66

7-
<CodeFile file="greet.ts" range="27-35" highlight="28,31" />
7+
```ts filename=greet.ts highlight=2,5 nonumber
8+
function test(title: string, callback: () => void) {
9+
try {
10+
callback()
11+
console.log(`✓ ${title}`)
12+
} catch (error) {
13+
console.error(`✗ ${title}`)
14+
console.error(error, '\n')
15+
}
16+
}
17+
```
818

919
Then, I wrap our existing tests in the `test()` function, giving it a meaningful title so the expectation behind each test is clear.
1020

11-
<CodeFile file="greet.ts" range="9-15" highlight="9,13" />
21+
```ts filename=greet.ts highlight=1,5 nonumber
22+
test('returns a greeting message for the given name', () => {
23+
expect(greet('John')).toBe('Hello, John!')
24+
})
25+
26+
test('returns a congratulation message for the given name', () => {
27+
expect(congratulate('Sarah')).toBe('Congrats, Sarah!')
28+
})
29+
```
1230

1331
Now, whenever a test fails, we can immediately see its title and the relevant assertion error below.
1432

exercises/02.test-structure/03.solution.test-files/README.mdx

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,76 @@
44

55
I start by creating a new `greet.test.ts` file and moving only the `test(...)` blocks there.
66

7-
<CodeFile file="greet.test.ts" range="3-9" />
7+
```ts filename=greet.test.ts nonumber
8+
test('returns a greeting message for the given name', () => {
9+
expect(greet('John')).toBe('Hello, John!')
10+
})
11+
12+
test('returns a congratulation message for the given name', () => {
13+
expect(congratulate('Sarah')).toBe('Congrats, Sarah!')
14+
})
15+
```
816

917
<callout-info>Which suffix to use: `.spec.ts` or `.test.ts`, or both?</callout-info>
1018

1119
If I try to run test test file now, it will exit on undefined `greet()` and `congratulation()` functions because they are neither defined nor imported from anywhere.
1220

1321
So I go and export those functions from the `greet.ts` module:
1422

15-
<CodeFile file="greet.ts" range="1-7" highlight="1,5" />
23+
```ts filename=greet.ts highlight=1,5
24+
export function greet(name: string) {
25+
return `Hello, ${name}!`
26+
}
27+
28+
export function congratulate(name: string) {
29+
return `Congrats, ${name}!`
30+
}
31+
```
1632

1733
And import them in the test file:
1834

19-
<CodeFile file="greet.test.ts" range="1-9" highlight="1" />
35+
```ts filename=greet.test.ts add=1
36+
import { greet, congratulate } from './greet.js'
37+
```
2038

2139
Next, I want to make the `test()` and `expect()` functions available _globally_. Every test will be using those, so there's no need to explicitly import them every time.
2240

2341
Next, I create a `setup.ts` file where I start by describing the `test()` and `expect()` functions in TypeScript. I add them to the `global` namespace to let TypeScript know those functions will be available globally, and that we don't have to import them.
2442

25-
<CodeFile file="setup.ts" range="1-8" highlight="5-8" />
43+
```ts filename=setup.ts highlight=5-8 nonumber
44+
interface Assertions {
45+
toBe(expected: unknown): void
46+
}
47+
48+
declare global {
49+
var expect: (actual: unknown) => Assertions
50+
var test: (title: string, callback: () => void) => void
51+
}
52+
```
2653

2754
Then, I move the existing `test()` and `expect()` functions directly to the `globalThis` object to expose them globally on _runtime_ and also benefit from the type inference since they are now fully annotated!
2855

29-
<CodeFile file="setup.ts" range="10-29" highlight="10,20" />
56+
```ts filename=setup.ts highlight=1,11 nonumber
57+
globalThis.expect = function (actual) {
58+
return {
59+
toBe(expected: unknown) {
60+
if (actual !== expected) {
61+
throw new Error(`Expected ${actual} to equal to ${expected}`)
62+
}
63+
},
64+
}
65+
}
66+
67+
globalThis.test = function (title, callback) {
68+
try {
69+
callback()
70+
console.log(`✓ ${title}`)
71+
} catch (error) {
72+
console.error(`✗ ${title}`)
73+
console.error(error, '\n')
74+
}
75+
}
76+
```
3077

3178
All that remains is to verify that the tests are running correctly.
3279

exercises/02.test-structure/04.problem.hooks/README.mdx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,21 @@
44

55
One day, our Peter the Project Manager comes to us with a great idea to improve the app. He suggests that we wish our users a happy day as a part of the greeting message (a bit of kindness goes a long way). That sounds easy enough, and so we change the `greet()` function to reflect that suggestion:
66

7-
<CodeFile file="greet.ts" range="1-5" highlight="4" />
7+
```ts filename=greet.ts highlight=4 nonumber
8+
export function greet(name: string) {
9+
const weekday = new Date().toLocaleDateString('en-US', { weekday: 'long' })
10+
11+
return `Hello, ${name}! Happy, ${weekday}.`
12+
}
13+
```
814

915
Since the intention behind the code has changed (now it also includes the day of the week), we should adjust the relevant tests to capture that:
1016

11-
<CodeFile file="greet.test.ts" range="26-28" highlight="27" />
17+
```ts filename=greet.ts highlight=2 nonumber
18+
test('returns a greeting message for the given name', () => {
19+
expect(greet('John')).toBe('Hello, John! Happy, Monday.')
20+
})
21+
```
1222

1323
The issue with this test is that it will only pass on Mondays! That won't do. We need a _deterministic_ test, no matter where or when we run it. To fix this, let's first understand why this happens.
1424

exercises/02.test-structure/04.solution.hooks/README.mdx

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,59 @@
44

55
First, I add the `beforeAll()` and `afterAll()` function declarations to the `global` namespace in TypeScript:
66

7-
<CodeFile file="setup.ts" range="1-10" highlight="8-9" />
7+
```ts filename=setup.ts highlight=8-9 nonumber
8+
interface Assertions {
9+
toBe(expected: unknown): void
10+
}
11+
12+
declare global {
13+
var expect: (actual: unknown) => Assertions
14+
var test: (title: string, callback: () => void) => void
15+
var beforeAll: (callback: () => void) => void
16+
var afterAll: (callback: () => void) => void
17+
}
18+
```
819

920
Then, I implement the `beforeAll()` function, which invokes the given `callback` immediately.
1021

11-
<CodeFile file="setup.ts" range="32-34" highlight="33" />
22+
```ts filename=setup.ts highlight=2 nonumber
23+
globalThis.beforeAll = function (callback) {
24+
callback()
25+
}
26+
```
1227

1328
<callout-warning>Here, I'm relying on the fact that the `beforeAll()` function will be called _before_ any individual tests. Actual testing frameworks usually have a runner responsible for internally orchestrating hooks and tests regardless of the invocation order.</callout-warning>
1429

1530
The `afterAll` function will be a bit different. To invoke the `callback` once the tests are done, I will utilize the `beforeExit` event of a Node.js process to let me know when the test run is about to exit.
1631

17-
<CodeFile file="setup.ts" range="36-40" highlight="37-39" />
32+
```ts filename=setup.ts highlight=2-4 nonumber
33+
globalThis.afterAll = function (callback) {
34+
process.on('beforeExit', () => {
35+
callback()
36+
})
37+
}
38+
```
1839

1940
Then, I go to the `greet.test.ts` and add the `beforeAll()` hook that patches the global `Date` constructor and uses the stored `OriginalDate` class to create a fixed date.
2041

21-
<CodeFile file="greet.test.ts" range="3-9" highlight="3,6-8" />
42+
```ts filename=greet.test.ts highlight=1,4-6 nonumber
43+
const OriginalDate = globalThis.Date
44+
45+
beforeAll(() => {
46+
globalThis.Date = new Proxy(globalThis.Date, {
47+
construct: () => new OriginalDate('2024-01-01'),
48+
})
49+
})
50+
```
2251

2352
<callout-warning>I recommend providing an entire UTC date, including an explicit timezone, as the value of the mocked date to have a resilient test setup: `new OriginalDate('2024-01-01 00:00:00.000Z')`</callout-warning>
2453

2554
Similarly, I make sure to clean up this `Date` mock in the `afterAll()` hook:
2655

27-
<CodeFile file="greet.test.ts" range="11-13" highlight="12" />
56+
```ts filename=greet.test.ts highlight=2 nonumber
57+
afterAll(() => {
58+
globalThis.Date = OriginalDate
59+
})
60+
```
2861

2962
<callout-success>Remember the rule of clean hooks: have a cleanup for each side effect in your setup (e.g. spawn a server → close the server; patch a global → restore the global).</callout-success>

0 commit comments

Comments
 (0)