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
8 changes: 6 additions & 2 deletions exercises/01.principles/01.problem.intentions/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@

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

<CodeFile file="greet.ts" nocopy />
```ts filename=greet.js nocopy nonumber
function greet(name: string) {
return `Hello, ${name}!`
}
```

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

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

```js
```ts
// Run the code and get the *actual* result.
let result = code(...args)

Expand Down
12 changes: 10 additions & 2 deletions exercises/01.principles/01.solution.intentions/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,19 @@

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

<CodeFile file="greet.ts" range="5" />
```ts filename=greet.ts nonumber
let message = greet('John')
```

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.

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

Let's run our `greet.ts` file now and see what happens.

```sh
```sh nonumber
npx tsx greet.ts
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@ The first thing I do whenever a test fails, I look at the _assertions_ (comparis

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

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

if (message !== 'Hello, John!') {
throw new Error(
`Expected message to equal to "Hello, John!" but got "${message}"`,
)
}
```

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

Expand Down
12 changes: 10 additions & 2 deletions exercises/02.test-structure/01.problem.assertions/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,21 @@ We don't have any special setup to test the `greet` function. The setup phase is

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

<CodeFile file="greet.ts" range="9" nocopy />
```ts filename=greet.ts nocopy nonumber
let message = greet('John')
```

### Assertion

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

<CodeFile file="greet.ts" range="11-15" nocopy />
```ts filename=greet.ts nocopy nonumber
if (message !== 'Hello, John!') {
throw new Error(
`Expected message to equal to "Hello, John!" but got "${message}"`,
)
}
```

## The problem

Expand Down
17 changes: 15 additions & 2 deletions exercises/02.test-structure/01.solution.assertions/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,24 @@

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

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

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

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

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.

Expand Down
22 changes: 20 additions & 2 deletions exercises/02.test-structure/02.solution.test-blocks/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,29 @@

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.

<CodeFile file="greet.ts" range="27-35" highlight="28,31" />
```ts filename=greet.ts highlight=2,5 nonumber
function test(title: string, callback: () => void) {
try {
callback()
console.log(`✓ ${title}`)
} catch (error) {
console.error(`✗ ${title}`)
console.error(error, '\n')
}
}
```

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

<CodeFile file="greet.ts" range="9-15" highlight="9,13" />
```ts filename=greet.ts highlight=1,5 nonumber
test('returns a greeting message for the given name', () => {
expect(greet('John')).toBe('Hello, John!')
})

test('returns a congratulation message for the given name', () => {
expect(congratulate('Sarah')).toBe('Congrats, Sarah!')
})
```

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

Expand Down
57 changes: 52 additions & 5 deletions exercises/02.test-structure/03.solution.test-files/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,76 @@

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

<CodeFile file="greet.test.ts" range="3-9" />
```ts filename=greet.test.ts nonumber
test('returns a greeting message for the given name', () => {
expect(greet('John')).toBe('Hello, John!')
})

test('returns a congratulation message for the given name', () => {
expect(congratulate('Sarah')).toBe('Congrats, Sarah!')
})
```

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

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.

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

<CodeFile file="greet.ts" range="1-7" highlight="1,5" />
```ts filename=greet.ts highlight=1,5
export function greet(name: string) {
return `Hello, ${name}!`
}

export function congratulate(name: string) {
return `Congrats, ${name}!`
}
```

And import them in the test file:

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

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.

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.

<CodeFile file="setup.ts" range="1-8" highlight="5-8" />
```ts filename=setup.ts highlight=5-8 nonumber
interface Assertions {
toBe(expected: unknown): void
}

declare global {
var expect: (actual: unknown) => Assertions
var test: (title: string, callback: () => void) => void
}
```

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!

<CodeFile file="setup.ts" range="10-29" highlight="10,20" />
```ts filename=setup.ts highlight=1,11 nonumber
globalThis.expect = function (actual) {
return {
toBe(expected: unknown) {
if (actual !== expected) {
throw new Error(`Expected ${actual} to equal to ${expected}`)
}
},
}
}

globalThis.test = function (title, callback) {
try {
callback()
console.log(`✓ ${title}`)
} catch (error) {
console.error(`✗ ${title}`)
console.error(error, '\n')
}
}
```

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

Expand Down
14 changes: 12 additions & 2 deletions exercises/02.test-structure/04.problem.hooks/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,21 @@

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:

<CodeFile file="greet.ts" range="1-5" highlight="4" />
```ts filename=greet.ts highlight=4 nonumber
export function greet(name: string) {
const weekday = new Date().toLocaleDateString('en-US', { weekday: 'long' })

return `Hello, ${name}! Happy, ${weekday}.`
}
```

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:

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

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.

Expand Down
43 changes: 38 additions & 5 deletions exercises/02.test-structure/04.solution.hooks/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,59 @@

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

<CodeFile file="setup.ts" range="1-10" highlight="8-9" />
```ts filename=setup.ts highlight=8-9 nonumber
interface Assertions {
toBe(expected: unknown): void
}

declare global {
var expect: (actual: unknown) => Assertions
var test: (title: string, callback: () => void) => void
var beforeAll: (callback: () => void) => void
var afterAll: (callback: () => void) => void
}
```

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

<CodeFile file="setup.ts" range="32-34" highlight="33" />
```ts filename=setup.ts highlight=2 nonumber
globalThis.beforeAll = function (callback) {
callback()
}
```

<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>

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.

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

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.

<CodeFile file="greet.test.ts" range="3-9" highlight="3,6-8" />
```ts filename=greet.test.ts highlight=1,4-6 nonumber
const OriginalDate = globalThis.Date

beforeAll(() => {
globalThis.Date = new Proxy(globalThis.Date, {
construct: () => new OriginalDate('2024-01-01'),
})
})
```

<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>

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

<CodeFile file="greet.test.ts" range="11-13" highlight="12" />
```ts filename=greet.test.ts highlight=2 nonumber
afterAll(() => {
globalThis.Date = OriginalDate
})
```

<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>
16 changes: 13 additions & 3 deletions exercises/03.async/01.problem.await/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,29 @@

In our application, we use the `greet()` function a lot in conjunction with fetching the user from a server. To simplify this usage, we've introduced a new function called `greetByResponse()` that accepts a Fetch API `Response` instance representing the user response from the server.

<CodeFile file="greet.ts" range="7-10" nocopy />
```ts filename=greet.ts nonumber nocopy
export async function greetByResponse(response: Response) {
const user = await response.json()
return greet(user.firstName)
}
```

So now, whenever we need to greet a fetched user, we can use this new function:

```ts nocopy
```ts nocopy nonumber
fecth('/api/user')
.then(response => greetByResponse(response))
.then(greeting => render(greeting))
```

Next, we've added a test case for the `greetByResponse()` function. Since it's marked as `async`, it will return a promise when run. We will account for that promise by making the test callback `async` and `await`'ing the result of the `greetByResponse()` function call in the test:

<CodeFile file="greet.test.ts" range="19-22" highlight="19,21" nocopy />
```ts filename=greet.test.ts highlight=1,3 nocopy nonumber
test('returns a greeting message for the given user response', async () => {
const response = Response.json({ name: 'Patrick' })
expect(await greetByResponse(response)).toBe('Hello, Patrick! Happy, Monday.')
})
```

All the tests are passing once we run them but there's also some assertion error printed _after_ the tests. It looks suspiciously related to `greetByResponse()`.

Expand Down
26 changes: 23 additions & 3 deletions exercises/03.async/01.solution.await/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,35 @@

I will start by making the `test()` function in `setup.ts` asynchronous.

<CodeFile file="setup.ts" range="22" highlight="22" />
```ts filename=setup.ts remove=1 add=2 nonumber
globalThis.test = function (title, callback) {
globalThis.test = async function (title, callback) {
```

This will allow me to await the `callback()` if it happens to be `async` too.

<CodeFile file="setup.ts" range="22-26" highlight="24" />
```ts filename=setup.ts highlight=3 nonumber
globalThis.test = async function (title, callback) {
try {
await callback()
console.log(`✓ ${title}`)
} catch (error) {
console.error(`✗ ${title}`)
console.error(error, '\n')
}
}
```

<callout-info>To let TypeScript know that `callback` is now, potentially, an asynchronous function, I will adjust its return type to include `Promise<void>`:</callout-info>

<CodeFile file="setup.ts" range="5-10" highlight="7" />
```ts filename=setup.ts highlight=3 nonumber
declare global {
var expect: (actual: unknown) => Assertions
var test: (title: string, callback: () => void | Promise<void>) => void
var beforeAll: (callback: () => void) => void
var afterAll: (callback: () => void) => void
}
```

If I run the tests now, I can correctly see the assertion on `greetByResponse()` failing the relevant test:

Expand Down
Loading