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
53 changes: 2 additions & 51 deletions exercises/01.boundaries/01.problem.boundaries/README.mdx
Original file line number Diff line number Diff line change
@@ -1,54 +1,5 @@
# Test boundaries

You will be testing all sorts of code in the wild. Some of it will be simple and self-contained, and some will be tangled with dependencies and side effects. Test boundaries is what helps you to untangle more complex code, focusing on the exact [intention](https://www.epicweb.dev/the-true-purpose-of-testing) you wish to test.
<EpicVideo url="https://www.epicweb.dev/workshops/mocking-techniques-in-vitest/boundaries/test-boundaries" />

To help you visualize what mocking does to your tested code I built a little app called [How Mocking Works](https://howmocking.works). Let's explore it together as a part of this exercise.

<callout-info>**You don't have to code anything in this exercise**. There's plenty of coding stored for you later! For now, let's have a proper introduction into mocking with a bit of visual learning.</callout-info>

![How Mocking Works?](https://howmocking.works/og.jpg)

## Mocking visualized

In the app, I represented various JavaScript modules, like `one.js` and `two.js` with geometric shapes, while the dependencies between them with colored lines. The graph that you see represents the extent of the tested code if you are writing a test for the `one.js` module.

By default, _all of the `one.js` dependencies run in the test_. By clicking on the links (lines) between various modules and their methods you can "cut off" (or mock) everything that goes past that link.

The orange double-line injected into the dependency link represents a _mock_. It can be anything, from intercepting an HTTP call to mocking the method or the entire module. Those are the technical details of the mock. No matter what mocking technique you use, it ends up trimming your dependency tree to the scope you need in your test.

## Overmocking

Mocking is a powerful tool. It is crucial you learn from the start that it has to be used sparingly and with a clear purpose. If misused, it can result in tests that don't actually test anything or make your testing experience far more difficult than it has to be.

There are a couple of rules to keep your mocking superpowers at bay:

### Never mock anything directly related to the intention you want to test

If you are testing what happens when the user clicks a button, you mustn't mock that button's behavior. If you do, your test will be testing your mock, and that's not what you want.

> You can reproduce this scenario visually by putting a mock between the `one.js` module and its dependency on the `a()` method. Nothing will be tested if you do!

It may not always be as obvious to determine what's related to the tested intention and what's not. For example, when testing your `Emitter.prototype.emit` method to make sure it calls the previously added listeners, it may be tempting to assume that the listener that's being called is related to the intention.

```ts
const emitter = new Emitter()
emitter.on('event', listener)
emitter.emit('event')
// Make sure the "listener" function is called!
```

But the `listener` function itself is not the point. In fact, it can be anything. Its implementation does not influence what you want to test—that _any_ previously added listener gets called when the `event` is emitted.

> :scroll: Follow the [Golden Rule of Assertions](https://www.epicweb.dev/the-golden-rule-of-assertions) to see what influences the result of your test and what doesn't.

### Always mock third-party HTTP calls

No matter the testing level, you must always omit the HTTP calls to _third-party services_ from your test suite. They may seem important but they never are. Your tests can still focus on whether _your application_ performs the necessary requests to those services but their operability must not influence the result of your tests.

> You do not own third-party systems and as such, they mustn't be allowed to affect your test.

### Choose the least intrusive mocking technique possible

In other words, draw the most permissive test boundary that you can.

Coming back to our visual example, if you want to exclude `b()` from the test, mock the `two.js` [dependency on `b()`](https://howmocking.works/#7) and not [the entire `two.js` module](https://howmocking.works/#2). By putting the mock too "high" up the dependency tree, you may lose the important behaviors of your code even if they don't seem related to what you are testing. But most likely, you would end up re-implementing the missing pieces because now _you_ become in charge of the entire `two.js` module.
I like to start with seemingly basic and straightforward exercises because they end up growing in value as you progress through the material (and beyond). The test boundary is one of such examples. In the end, the entire purpose of mocking is to draw that boundary, and you cannot wield mocking efficiently without understanding it first.
55 changes: 54 additions & 1 deletion exercises/01.boundaries/01.solution.boundaries/README.mdx
Original file line number Diff line number Diff line change
@@ -1,3 +1,56 @@
# Test boundaries

I like the start topics with seemingly basic and straightforward exercises because they end up growing in value as you progress through the material (and beyond). The test boundary is one of such examples. In the end, the entire purpose of mocking is to draw that boundary, and you cannot wield mocking efficiently without understanding it first.
<EpicVideo url="https://www.epicweb.dev/workshops/mocking-techniques-in-vitest/boundaries/test-boundaries/solution" />

You will be testing all sorts of code in the wild. Some of it will be simple and self-contained, and some will be tangled with dependencies and side effects. Test boundaries is what helps you to untangle more complex code, focusing on the exact [intention](https://www.epicweb.dev/the-true-purpose-of-testing) you wish to test.

To help you visualize what mocking does to your tested code I built a little app called [How Mocking Works](https://howmocking.works). Let's explore it together as a part of this exercise.

<callout-info>**You don't have to code anything in this exercise**. There's plenty of coding stored for you later! For now, let's have a proper introduction into mocking with a bit of visual learning.</callout-info>

![How Mocking Works?](https://howmocking.works/og.jpg)

## Mocking visualized

In the app, I represented various JavaScript modules, like `one.js` and `two.js` with geometric shapes, while the dependencies between them with colored lines. The graph that you see represents the extent of the tested code if you are writing a test for the `one.js` module.

By default, _all of the `one.js` dependencies run in the test_. By clicking on the links (lines) between various modules and their methods you can "cut off" (or mock) everything that goes past that link.

The orange double-line injected into the dependency link represents a _mock_. It can be anything, from intercepting an HTTP call to mocking the method or the entire module. Those are the technical details of the mock. No matter what mocking technique you use, it ends up trimming your dependency tree to the scope you need in your test.

## Overmocking

Mocking is a powerful tool. It is crucial you learn from the start that it has to be used sparingly and with a clear purpose. If misused, it can result in tests that don't actually test anything or make your testing experience far more difficult than it has to be.

There are a couple of rules to keep your mocking superpowers at bay:

### Never mock anything directly related to the intention you want to test

If you are testing what happens when the user clicks a button, you mustn't mock that button's behavior. If you do, your test will be testing your mock, and that's not what you want.

> You can reproduce this scenario visually by putting a mock between the `one.js` module and its dependency on the `a()` method. Nothing will be tested if you do!

It may not always be as obvious to determine what's related to the tested intention and what's not. For example, when testing your `Emitter.prototype.emit` method to make sure it calls the previously added listeners, it may be tempting to assume that the listener that's being called is related to the intention.

```ts
const emitter = new Emitter()
emitter.on('event', listener)
emitter.emit('event')
// Make sure the "listener" function is called!
```

But the `listener` function itself is not the point. In fact, it can be anything. Its implementation does not influence what you want to test—that _any_ previously added listener gets called when the `event` is emitted.

> :scroll: Follow the [Golden Rule of Assertions](https://www.epicweb.dev/the-golden-rule-of-assertions) to see what influences the result of your test and what doesn't.

### Always mock third-party HTTP calls

No matter the testing level, you must always omit the HTTP calls to _third-party services_ from your test suite. They may seem important but they never are. Your tests can still focus on whether _your application_ performs the necessary requests to those services but their operability must not influence the result of your tests.

> You do not own third-party systems and as such, they mustn't be allowed to affect your test.

### Choose the least intrusive mocking technique possible

In other words, draw the most permissive test boundary that you can.

Coming back to our visual example, if you want to exclude `b()` from the test, mock the `two.js` [dependency on `b()`](https://howmocking.works/#7) and not [the entire `two.js` module](https://howmocking.works/#2). By putting the mock too "high" up the dependency tree, you may lose the important behaviors of your code even if they don't seem related to what you are testing. But most likely, you would end up re-implementing the missing pieces because now _you_ become in charge of the entire `two.js` module.
2 changes: 2 additions & 0 deletions exercises/01.boundaries/FINISHED.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Boundaries

<EpicVideo url="https://www.epicweb.dev/workshops/mocking-techniques-in-vitest/boundaries/recap-of-boundaries" />

Understanding test boundaries helps you think about mocking as a tool. A tool that helps you balance what matters in the test vs what doesn't. That's really what mocking of any kind is about.

Ultimately, it is _your_ decision when to draw that line. Just know that it will affect what kind of test you get in return.
Expand Down
2 changes: 2 additions & 0 deletions exercises/01.boundaries/README.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Boundaries

<EpicVideo url="https://www.epicweb.dev/workshops/mocking-techniques-in-vitest/boundaries/intro-to-boundaries" />

There has been almost two decades since mocks (originally called "_test doubles_") were first defined. If you go and search for "mocks" online right now, you will find a plethora of technical explanations and usage examples on how to substitute, spy, stub, replace, fake, and do all sorts of things with the tested code. Most of those resources put emphasis on different ways to mock but, somehow, seldom bother to explain what _is_ a mock, _when_ would you reach out for one, and _what_ it actually does to your code.

## What is mocking?
Expand Down
2 changes: 2 additions & 0 deletions exercises/02.functions/01.problem.mock-functions/README.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Mock functions

<EpicVideo url="https://www.epicweb.dev/workshops/mocking-techniques-in-vitest/functions/mock-functions" />

We have an `Emitter` implementation that can add listeners to events and call those listeners whenever a matching event is emitted. This behavior is achieved by implementing two core methods on the emitter: `.on()` and `.emit()`:

<CodeFile file="emitter.ts" range="7-22" nocopy />
Expand Down
2 changes: 2 additions & 0 deletions exercises/02.functions/01.solution.mock-functions/README.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Mock functions

<EpicVideo url="https://www.epicweb.dev/workshops/mocking-techniques-in-vitest/functions/mock-functions/solution" />

In the `emitter.test.ts`, I will start from creating the `listener` function but it won't be a regular JavaScript function. Instead, I will use the `vi.fn()` API from Vitest, which creates a _special_ kind of function.

<CodeFile file="emitter.test.ts" range="5" />
Expand Down
2 changes: 2 additions & 0 deletions exercises/02.functions/02.problem.spies/README.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Spies

<EpicVideo url="https://www.epicweb.dev/workshops/mocking-techniques-in-vitest/functions/spies" />

Testing the intentions around side effects is always tricky. Like this `UserService` class that's supposed to log out the `createUser` event whenever a new user is created:

<CodeFile file="user-service.ts" highlight="7" nocopy />
Expand Down
2 changes: 2 additions & 0 deletions exercises/02.functions/02.solution.spies/README.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Spies

<EpicVideo url="https://www.epicweb.dev/workshops/mocking-techniques-in-vitest/functions/spies/solution" />

Let's start by creating a _spy_ for the `logger.log()` method using the [`vi.spyOn()`](https://vitest.dev/api/vi.html#vi-spyon) function from Vitest:

<CodeFile file="user-service.test.ts" range="5-7" highlight="6" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Mock implementation

<EpicVideo url="https://www.epicweb.dev/workshops/mocking-techniques-in-vitest/functions/mock-implementation" />

In this one, we have an `OrderController` class responsible for handling orders in our eshop. It has a `.createOrder()` method that accepts a `cart` object and does the following:

1. Throws an error if any of the cart items are out of stock;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Mock implementation

<EpicVideo url="https://www.epicweb.dev/workshops/mocking-techniques-in-vitest/functions/mock-implementation/solution" />

Let's start by going to the first test case for our `OrderController` and spying on the `isItemInStock` method of the created `controller` instance:

<CodeFile file="order-controller.test.ts" range="3-6" highlight="6" />
Expand Down
2 changes: 2 additions & 0 deletions exercises/02.functions/FINISHED.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Functions

<EpicVideo url="https://www.epicweb.dev/workshops/mocking-techniques-in-vitest/functions/recap-of-functions" />

Congratulations! :tada: You've completed the exercises on functions! Now you know how to create mock functions, spy on function calls, and mock the implementation to model and test the behaviors you need.

We will be circling back to what you've learned in this block in the exercises to come, since a lot of things in JavaScript are represented by functions!
Expand Down
2 changes: 2 additions & 0 deletions exercises/02.functions/README.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Functions

<EpicVideo url="https://www.epicweb.dev/workshops/mocking-techniques-in-vitest/functions/intro-to-functions" />

Functions is the heart and soul of any logic in JavaScript. Event handlers, callbacks, utilities—you have likely worked with functions before. Whenever you want your code to _do_ something you reach out to a function because it represents _action_.

Ideally, testing functions should come down to the I/O (input/output) testing. You provide a function with the right input and assert the expected output. This testing strategy is extremely prevalent in unit testing and it truly shines with pure functions.
Expand Down
2 changes: 2 additions & 0 deletions exercises/03.date-and-time/01.problem.date-time/README.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Date and time

<EpicVideo url="https://www.epicweb.dev/workshops/mocking-techniques-in-vitest/date-and-time/date-and-time" />

In our application, we need to display a relative time label, like "1 minute ago", "3 days ago", etc. We've created a `getRelativeTime()` function to achieve that. It takes any given date and calculates how far ago it was compared to `Date.now()`, returning a formatted string.

<CodeFile file="get-relative-time.ts" />
Expand Down
2 changes: 2 additions & 0 deletions exercises/03.date-and-time/01.solution.date-time/README.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Date and time

<EpicVideo url="https://www.epicweb.dev/workshops/mocking-techniques-in-vitest/date-and-time/date-and-time/solution" />

You may be noticing a pattern here. I'm trying to keep mock definitions contained and use the hooks like `beforeAll()` and `afterAll()` to make sure no mocks are left once the tests are done (and, often, even between the tests).

Testing the `getRelativeTime()` function will be no exception:
Expand Down
2 changes: 2 additions & 0 deletions exercises/03.date-and-time/02.problem.timers/README.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Timers

<EpicVideo url="https://www.epicweb.dev/workshops/mocking-techniques-in-vitest/date-and-time/timers" />

[Debounce and throttle](https://kettanaito.com/blog/debounce-vs-throttle) have to be some of my favorite utility functions, and they both happened to rely on timers to work. Let's study them in more detail and also see how we would test them.

Take a look at the `debounce` function implementation below:
Expand Down
2 changes: 2 additions & 0 deletions exercises/03.date-and-time/02.solution.timers/README.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Timers

<EpicVideo url="https://www.epicweb.dev/workshops/mocking-techniques-in-vitest/date-and-time/timers/solution" />

I begin from setting up the testing hooks to mock time in this test, using the `vi.useFakeTimers()` and `vi.useRealTimers()` functions, the same way I did in the previous exercise:

<CodeFile file="debounce.test.ts" range="3-9" highlight="4,8" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Ticks and tasks

<EpicVideo url="https://www.epicweb.dev/workshops/mocking-techniques-in-vitest/date-and-time/ticks-and-tasks" />

Since we are touching on the topic of testing logic that executes at a different time, we have to talk about the event loop. JavaScript is a single-threaded language, which means that it executes the operations in your app one-by-one, making them _blocking_ (the next piece of code cannot run until the previous one has finished). To know what code to run, JavaScript uses an _event loop_.

Whenever an action happens in your code, it's added to the execution stack—a queue of all the operations to run. As the name suggests, the event loop constantly iterates over that stack to see if there's anything else left in it to run. Once the stack is empty, it frees the thread so it can schedule next operations, and so on.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Ticks and tasks

<EpicVideo url="https://www.epicweb.dev/workshops/mocking-techniques-in-vitest/date-and-time/ticks-and-tasks/solution" />

First thing to mention is that the usual `vi.useFakeTimers()` does _not_ mock `queueMicrotask()`. To enable that, I will provide it with the `toFake` argument configured to include `'queueMicrotask'` in the date mocks:

<CodeFile file="connection.test.ts" range="3-11" highlight="4-6" />
Expand Down
2 changes: 2 additions & 0 deletions exercises/03.date-and-time/FINISHED.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Date and time

<EpicVideo url="https://www.epicweb.dev/workshops/mocking-techniques-in-vitest/date-and-time/recap-of-date-and-time" />

Hooray! 🥳 Great job on nailing these exercises!

Now you know how to mock all sorts of time-sensitive things, from date and time, to timers and event loops. So the next time you need to test something that involves date, don't hesitate to employ your sourcery and freeze the time itself.
Expand Down
2 changes: 2 additions & 0 deletions exercises/03.date-and-time/README.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Date and time

<EpicVideo url="https://www.epicweb.dev/workshops/mocking-techniques-in-vitest/date-and-time/intro-to-date-and-time" />

If you've worked with date, time, or timezones in the past, you know how quickly it can become a complete pain. The time is constantly changing. Likely, a few seconds have already passed since you've started reading this sentence. If I had to write a test for how long it will take you to read this entire exercise, I'd have to resort to guessing. There's just so many things that affect that time!

Duration isn't the only variable here either. Date can also differ based on the timezone of the computer that creates it. In other words, you and your colleague might get different test results for the same test if they happened to live half a world away from you. Do you have to guess a better date for that test now...?
Expand Down
2 changes: 2 additions & 0 deletions exercises/04.globals/01.problem.global-methods/README.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Global methods

<EpicVideo url="https://www.epicweb.dev/workshops/mocking-techniques-in-vitest/globals/global-methods" />

Often, global behaviors you would want to mock will be encapsulated to particular global objects. For example, the `log()` method is a part of the `console` object, which means you don't have to mock the entire `console` just to know when it prints something.

In that regard, mocking global methods is no different than mocking or spying on regular functions!
Expand Down
2 changes: 2 additions & 0 deletions exercises/04.globals/01.solution.global-methods/README.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Global methods

<EpicVideo url="https://www.epicweb.dev/workshops/mocking-techniques-in-vitest/globals/global-methods/solution" />

I start by going to the `beforeAll()` hook and adding a spy on the `console.log` method using the `vi.spyOn()` utility:

<CodeFile file="print-server-url.test.ts" range="3-5" />
Expand Down
2 changes: 2 additions & 0 deletions exercises/04.globals/02.problem.global-values/README.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Global values

<EpicVideo url="https://www.epicweb.dev/workshops/mocking-techniques-in-vitest/globals/global-values" />

`vi.spyOn()` is a great way to mock and control global methods. However, when your code depends on global _values_, you need to employ a different approach.

In this exercise, we are testing the `toAbsoluteUrl()` function that looks like this:
Expand Down
Loading
Loading