From 85ebe246d8d624eb79f7e52c0e6d174ba56c0ba1 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Fri, 11 Oct 2024 19:26:30 +0200 Subject: [PATCH 1/2] add video embeds --- .../01.problem.boundaries/README.mdx | 53 +----------------- .../01.solution.boundaries/README.mdx | 55 ++++++++++++++++++- exercises/01.boundaries/FINISHED.mdx | 2 + exercises/01.boundaries/README.mdx | 2 + .../01.problem.mock-functions/README.mdx | 2 + .../01.solution.mock-functions/README.mdx | 2 + .../02.functions/02.problem.spies/README.mdx | 2 + .../02.functions/02.solution.spies/README.mdx | 2 + .../03.problem.mock-implementation/README.mdx | 2 + .../README.mdx | 2 + exercises/02.functions/FINISHED.mdx | 2 + exercises/02.functions/README.mdx | 2 + .../01.problem.date-time/README.mdx | 2 + .../01.solution.date-time/README.mdx | 2 + .../02.problem.timers/README.mdx | 2 + .../02.solution.timers/README.mdx | 2 + .../03.problem.ticks-and-tasks/README.mdx | 2 + .../03.solution.ticks-and-tasks/README.mdx | 2 + exercises/03.date-and-time/FINISHED.mdx | 2 + exercises/03.date-and-time/README.mdx | 2 + .../01.problem.global-methods/README.mdx | 2 + .../01.solution.global-methods/README.mdx | 2 + .../02.problem.global-values/README.mdx | 2 + .../02.solution.global-values/README.mdx | 2 + .../README.mdx | 2 + .../README.mdx | 2 + exercises/04.globals/FINISHED.mdx | 2 + exercises/04.globals/README.mdx | 2 + .../01.problem.setup-msw/README.mdx | 2 + .../01.solution.setup-msw/README.mdx | 2 + .../02.problem.mock-responses/README.mdx | 2 + .../02.solution.mock-responses/README.mdx | 2 + .../03.problem.error-responses/README.mdx | 6 +- .../03.solution.error-responses/README.mdx | 2 + .../04.problem.network-errors/README.mdx | 2 + .../04.solution.network-errors/README.mdx | 2 + .../05.problem.response-delay/README.mdx | 2 + .../05.solution.response-delay/README.mdx | 2 + exercises/05.network/FINISHED.mdx | 2 + exercises/05.network/README.mdx | 2 + .../README.mdx | 2 + .../README.mdx | 2 + .../README.mdx | 2 + .../README.mdx | 2 + exercises/06.modules/FINISHED.mdx | 3 + exercises/06.modules/README.mdx | 2 + exercises/FINISHED.mdx | 2 + exercises/README.mdx | 4 +- 48 files changed, 152 insertions(+), 55 deletions(-) diff --git a/exercises/01.boundaries/01.problem.boundaries/README.mdx b/exercises/01.boundaries/01.problem.boundaries/README.mdx index af8c651..dc4b2a2 100644 --- a/exercises/01.boundaries/01.problem.boundaries/README.mdx +++ b/exercises/01.boundaries/01.problem.boundaries/README.mdx @@ -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. + -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. - -**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. - -![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. diff --git a/exercises/01.boundaries/01.solution.boundaries/README.mdx b/exercises/01.boundaries/01.solution.boundaries/README.mdx index cd9ce0b..68bd00f 100644 --- a/exercises/01.boundaries/01.solution.boundaries/README.mdx +++ b/exercises/01.boundaries/01.solution.boundaries/README.mdx @@ -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. + + +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. + +**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. + +![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. diff --git a/exercises/01.boundaries/FINISHED.mdx b/exercises/01.boundaries/FINISHED.mdx index 382a637..3a7ba37 100644 --- a/exercises/01.boundaries/FINISHED.mdx +++ b/exercises/01.boundaries/FINISHED.mdx @@ -1,5 +1,7 @@ # 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. diff --git a/exercises/01.boundaries/README.mdx b/exercises/01.boundaries/README.mdx index 20c9b04..35c021c 100644 --- a/exercises/01.boundaries/README.mdx +++ b/exercises/01.boundaries/README.mdx @@ -1,5 +1,7 @@ # 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? diff --git a/exercises/02.functions/01.problem.mock-functions/README.mdx b/exercises/02.functions/01.problem.mock-functions/README.mdx index bf6889b..a9cec27 100644 --- a/exercises/02.functions/01.problem.mock-functions/README.mdx +++ b/exercises/02.functions/01.problem.mock-functions/README.mdx @@ -1,5 +1,7 @@ # 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()`: diff --git a/exercises/02.functions/01.solution.mock-functions/README.mdx b/exercises/02.functions/01.solution.mock-functions/README.mdx index ef35173..47a36b7 100644 --- a/exercises/02.functions/01.solution.mock-functions/README.mdx +++ b/exercises/02.functions/01.solution.mock-functions/README.mdx @@ -1,5 +1,7 @@ # Mock functions + + 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. diff --git a/exercises/02.functions/02.problem.spies/README.mdx b/exercises/02.functions/02.problem.spies/README.mdx index ee9b383..ea442d0 100644 --- a/exercises/02.functions/02.problem.spies/README.mdx +++ b/exercises/02.functions/02.problem.spies/README.mdx @@ -1,5 +1,7 @@ # 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: diff --git a/exercises/02.functions/02.solution.spies/README.mdx b/exercises/02.functions/02.solution.spies/README.mdx index 6350965..e1004f7 100644 --- a/exercises/02.functions/02.solution.spies/README.mdx +++ b/exercises/02.functions/02.solution.spies/README.mdx @@ -1,5 +1,7 @@ # Spies + + 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: diff --git a/exercises/02.functions/03.problem.mock-implementation/README.mdx b/exercises/02.functions/03.problem.mock-implementation/README.mdx index 920272c..b1f1579 100644 --- a/exercises/02.functions/03.problem.mock-implementation/README.mdx +++ b/exercises/02.functions/03.problem.mock-implementation/README.mdx @@ -1,5 +1,7 @@ # 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; diff --git a/exercises/02.functions/03.solution.mock-implementation/README.mdx b/exercises/02.functions/03.solution.mock-implementation/README.mdx index 88a0733..d04a774 100644 --- a/exercises/02.functions/03.solution.mock-implementation/README.mdx +++ b/exercises/02.functions/03.solution.mock-implementation/README.mdx @@ -1,5 +1,7 @@ # Mock implementation + + Let's start by going to the first test case for our `OrderController` and spying on the `isItemInStock` method of the created `controller` instance: diff --git a/exercises/02.functions/FINISHED.mdx b/exercises/02.functions/FINISHED.mdx index e7e4156..41ac770 100644 --- a/exercises/02.functions/FINISHED.mdx +++ b/exercises/02.functions/FINISHED.mdx @@ -1,5 +1,7 @@ # 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! diff --git a/exercises/02.functions/README.mdx b/exercises/02.functions/README.mdx index 0ea2afa..09813cb 100644 --- a/exercises/02.functions/README.mdx +++ b/exercises/02.functions/README.mdx @@ -1,5 +1,7 @@ # 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. diff --git a/exercises/03.date-and-time/01.problem.date-time/README.mdx b/exercises/03.date-and-time/01.problem.date-time/README.mdx index d34eba5..6a5c8dc 100644 --- a/exercises/03.date-and-time/01.problem.date-time/README.mdx +++ b/exercises/03.date-and-time/01.problem.date-time/README.mdx @@ -1,5 +1,7 @@ # 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. diff --git a/exercises/03.date-and-time/01.solution.date-time/README.mdx b/exercises/03.date-and-time/01.solution.date-time/README.mdx index 084523c..c18b978 100644 --- a/exercises/03.date-and-time/01.solution.date-time/README.mdx +++ b/exercises/03.date-and-time/01.solution.date-time/README.mdx @@ -1,5 +1,7 @@ # Date and time + + 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: diff --git a/exercises/03.date-and-time/02.problem.timers/README.mdx b/exercises/03.date-and-time/02.problem.timers/README.mdx index 4e3e03c..60b7ce2 100644 --- a/exercises/03.date-and-time/02.problem.timers/README.mdx +++ b/exercises/03.date-and-time/02.problem.timers/README.mdx @@ -1,5 +1,7 @@ # 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: diff --git a/exercises/03.date-and-time/02.solution.timers/README.mdx b/exercises/03.date-and-time/02.solution.timers/README.mdx index c89eae5..e82d698 100644 --- a/exercises/03.date-and-time/02.solution.timers/README.mdx +++ b/exercises/03.date-and-time/02.solution.timers/README.mdx @@ -1,5 +1,7 @@ # Timers + + 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: diff --git a/exercises/03.date-and-time/03.problem.ticks-and-tasks/README.mdx b/exercises/03.date-and-time/03.problem.ticks-and-tasks/README.mdx index 3ca801f..e9c0223 100644 --- a/exercises/03.date-and-time/03.problem.ticks-and-tasks/README.mdx +++ b/exercises/03.date-and-time/03.problem.ticks-and-tasks/README.mdx @@ -1,5 +1,7 @@ # 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. diff --git a/exercises/03.date-and-time/03.solution.ticks-and-tasks/README.mdx b/exercises/03.date-and-time/03.solution.ticks-and-tasks/README.mdx index a092fda..186644e 100644 --- a/exercises/03.date-and-time/03.solution.ticks-and-tasks/README.mdx +++ b/exercises/03.date-and-time/03.solution.ticks-and-tasks/README.mdx @@ -1,5 +1,7 @@ # Ticks and tasks + + 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: diff --git a/exercises/03.date-and-time/FINISHED.mdx b/exercises/03.date-and-time/FINISHED.mdx index cd76ebe..cfc9570 100644 --- a/exercises/03.date-and-time/FINISHED.mdx +++ b/exercises/03.date-and-time/FINISHED.mdx @@ -1,5 +1,7 @@ # 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. diff --git a/exercises/03.date-and-time/README.mdx b/exercises/03.date-and-time/README.mdx index c95d2a3..344cc97 100644 --- a/exercises/03.date-and-time/README.mdx +++ b/exercises/03.date-and-time/README.mdx @@ -1,5 +1,7 @@ # 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...? diff --git a/exercises/04.globals/01.problem.global-methods/README.mdx b/exercises/04.globals/01.problem.global-methods/README.mdx index f5f989e..dd8007a 100644 --- a/exercises/04.globals/01.problem.global-methods/README.mdx +++ b/exercises/04.globals/01.problem.global-methods/README.mdx @@ -1,5 +1,7 @@ # 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! diff --git a/exercises/04.globals/01.solution.global-methods/README.mdx b/exercises/04.globals/01.solution.global-methods/README.mdx index 1fea1b8..2655ff7 100644 --- a/exercises/04.globals/01.solution.global-methods/README.mdx +++ b/exercises/04.globals/01.solution.global-methods/README.mdx @@ -1,5 +1,7 @@ # Global methods + + I start by going to the `beforeAll()` hook and adding a spy on the `console.log` method using the `vi.spyOn()` utility: diff --git a/exercises/04.globals/02.problem.global-values/README.mdx b/exercises/04.globals/02.problem.global-values/README.mdx index 641f6e2..b9cd270 100644 --- a/exercises/04.globals/02.problem.global-values/README.mdx +++ b/exercises/04.globals/02.problem.global-values/README.mdx @@ -1,5 +1,7 @@ # 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: diff --git a/exercises/04.globals/02.solution.global-values/README.mdx b/exercises/04.globals/02.solution.global-values/README.mdx index 5108f65..0c08881 100644 --- a/exercises/04.globals/02.solution.global-values/README.mdx +++ b/exercises/04.globals/02.solution.global-values/README.mdx @@ -1,5 +1,7 @@ # Global values + + As it often does, the test begins from the setup. In this case, the setup includes the introduction of both `beforeAll` and `afterAll` hooks to the test suite. diff --git a/exercises/04.globals/03.problem.environment-variables/README.mdx b/exercises/04.globals/03.problem.environment-variables/README.mdx index 4d14381..3e0204d 100644 --- a/exercises/04.globals/03.problem.environment-variables/README.mdx +++ b/exercises/04.globals/03.problem.environment-variables/README.mdx @@ -1,5 +1,7 @@ # Environment variables + + Another kind of global dependency your code may have is that on the environment. That is often done via _environment variables_, which, in JavaScript, are often exposed to the process via the `process.env` object. You reach out to environment variables in cases like managing API secrets, feature flags, implementing different environment-depending logic, etc. And as such, dependency on environment variable is also one you have to know how to mock to orchestrate your tests correctly. diff --git a/exercises/04.globals/03.solution.environment-variables/README.mdx b/exercises/04.globals/03.solution.environment-variables/README.mdx index af8f39c..aa0030c 100644 --- a/exercises/04.globals/03.solution.environment-variables/README.mdx +++ b/exercises/04.globals/03.solution.environment-variables/README.mdx @@ -1,5 +1,7 @@ # Environment variables + + There are multiple mocks working together in this test suite, so I make sure to establish them (and their cleanup) accordingly: diff --git a/exercises/04.globals/FINISHED.mdx b/exercises/04.globals/FINISHED.mdx index 735e130..ef345ff 100644 --- a/exercises/04.globals/FINISHED.mdx +++ b/exercises/04.globals/FINISHED.mdx @@ -1,5 +1,7 @@ # Globals + + Woah, another exercise done! :tada: Great job. This one has given you even more tools for your mocking arsenal, this time focusing on mocking globals. Whether it's mocking a value or a method, or an environment variable, you can utilize built-in utilities Vitest provides to make your testing life easier. diff --git a/exercises/04.globals/README.mdx b/exercises/04.globals/README.mdx index 83597de..42e8d83 100644 --- a/exercises/04.globals/README.mdx +++ b/exercises/04.globals/README.mdx @@ -1,5 +1,7 @@ # Globals + + At its core, mocking is a _dependency-management technique_. Whether you depend on things like date and time, particular built-in APIs or globals, other modules or calls to third-party services—knowing which dependencies to allow in a test affects the type and scope of that test. All dependencies you encounter can be divided into two groups: diff --git a/exercises/05.network/01.problem.setup-msw/README.mdx b/exercises/05.network/01.problem.setup-msw/README.mdx index a443b6b..97be246 100644 --- a/exercises/05.network/01.problem.setup-msw/README.mdx +++ b/exercises/05.network/01.problem.setup-msw/README.mdx @@ -1,5 +1,7 @@ # Set up MSW + + Setting up Mock Service Worker in your project consists of three steps: 1. Installing the library; diff --git a/exercises/05.network/01.solution.setup-msw/README.mdx b/exercises/05.network/01.solution.setup-msw/README.mdx index c516423..7872c91 100644 --- a/exercises/05.network/01.solution.setup-msw/README.mdx +++ b/exercises/05.network/01.solution.setup-msw/README.mdx @@ -1,5 +1,7 @@ # Set up MSW + + With MSW set up, we can now run `npm test` and see the outgoing request's method and URL printed during the test run: ```txt highlight=7 diff --git a/exercises/05.network/02.problem.mock-responses/README.mdx b/exercises/05.network/02.problem.mock-responses/README.mdx index 0520b9a..1c6dab5 100644 --- a/exercises/05.network/02.problem.mock-responses/README.mdx +++ b/exercises/05.network/02.problem.mock-responses/README.mdx @@ -1,5 +1,7 @@ # Mock responses + + Our `getAuthToken()` function depends on the response returned from the server quite singificantly. For starters, there's a validation layer to handle error responses: diff --git a/exercises/05.network/02.solution.mock-responses/README.mdx b/exercises/05.network/02.solution.mock-responses/README.mdx index 88d4cd8..60df40f 100644 --- a/exercises/05.network/02.solution.mock-responses/README.mdx +++ b/exercises/05.network/02.solution.mock-responses/README.mdx @@ -1,5 +1,7 @@ # Mock responses + + First, I will delete the `http.all()` request handler in `handlers.ts`: ```ts filename=handlers.ts remove=4-6 diff --git a/exercises/05.network/03.problem.error-responses/README.mdx b/exercises/05.network/03.problem.error-responses/README.mdx index e4cc02a..3b69a96 100644 --- a/exercises/05.network/03.problem.error-responses/README.mdx +++ b/exercises/05.network/03.problem.error-responses/README.mdx @@ -1,5 +1,7 @@ # Error responses + + Right now, we are sending the exact successful JSON response that the `getAuthToken()` expects. That allows us to reproduce and test the happy path behavior. But there are more behaviors that we intended for this function, and they happened to concern themselves with error handling. @@ -28,7 +30,7 @@ const server = setupServer( // This is a happy path handler. http.get('http://localhost/resource', () => { return Response.json({ response: 'happy' }) - }) + }), ) server.listen() @@ -41,7 +43,7 @@ server.use( // but returns a different mocked response. http.get('http://localhost/resource', () => { return Response.json({ response: 'override' }) - }) + }), ) await fetch('http://localhost/resource').then((response) => response.json()) diff --git a/exercises/05.network/03.solution.error-responses/README.mdx b/exercises/05.network/03.solution.error-responses/README.mdx index 33bb282..c14f253 100644 --- a/exercises/05.network/03.solution.error-responses/README.mdx +++ b/exercises/05.network/03.solution.error-responses/README.mdx @@ -1,5 +1,7 @@ # Error responses + + I start with restructuring our project a little bit. In order for me to use runtime handlers, I need to have access to the same `server` object created in `vitest.setup.ts`. However, importing from the setup file _is not recommended_. > :owl: Although Vitest will **cache imports** and not evaluate the global hooks twice if you import from the setup file, it caches imports **per test file**. This works nice in the isolated mode, which Vitest uses by default, but may cause problems if you switch to parallel test runs. diff --git a/exercises/05.network/04.problem.network-errors/README.mdx b/exercises/05.network/04.problem.network-errors/README.mdx index 7ab4f64..6423db3 100644 --- a/exercises/05.network/04.problem.network-errors/README.mdx +++ b/exercises/05.network/04.problem.network-errors/README.mdx @@ -1,5 +1,7 @@ # Network errors + + It's time to use API mocking to help us discover blind spots in our `getAuthToken()` function! One of the common mistakes when dealing with the network is forgetting about network error handling. Now, I'm not talking about error responses (we've handled those in the previous exercise) but about the scenarios when the _request fails altogether_. diff --git a/exercises/05.network/04.solution.network-errors/README.mdx b/exercises/05.network/04.solution.network-errors/README.mdx index 915c90e..7661320 100644 --- a/exercises/05.network/04.solution.network-errors/README.mdx +++ b/exercises/05.network/04.solution.network-errors/README.mdx @@ -1,5 +1,7 @@ # Network errors + + I will start from implementing the error handling for network errors in `get-auth-token.ts` module. Unlike error responses, network errors will [reject the `fetch` promise](https://kettanaito.com/blog/why-fetch-promise-doesnt-reject-on-error-responses). This means that I can handle those errors in the `catch` callback attached to that promise: diff --git a/exercises/05.network/05.problem.response-delay/README.mdx b/exercises/05.network/05.problem.response-delay/README.mdx index f5d274c..714afa3 100644 --- a/exercises/05.network/05.problem.response-delay/README.mdx +++ b/exercises/05.network/05.problem.response-delay/README.mdx @@ -1,5 +1,7 @@ # Response delay + + Let's conclude this exercise block with another improvement to the `getAuthToken()` function. Right now, we've accounted for the successful and error responses, and also for the network errors. However, network connectivity is tricky. What if the response _never arrives_ at all and just hangs forever? This can easily happen if the server is overloaded or the network is just too slow. diff --git a/exercises/05.network/05.solution.response-delay/README.mdx b/exercises/05.network/05.solution.response-delay/README.mdx index 1c24ee4..c27164e 100644 --- a/exercises/05.network/05.solution.response-delay/README.mdx +++ b/exercises/05.network/05.solution.response-delay/README.mdx @@ -1,5 +1,7 @@ # Response delay + + ## Timeout handling First, let's take a look at how the request timeout handling is implemented in the `getAuthToken()` function. Handling timeouts often comes down to introducing a _race condition_ between the asynchronous operation in question and the timeout itself. diff --git a/exercises/05.network/FINISHED.mdx b/exercises/05.network/FINISHED.mdx index 91f29c8..e455c53 100644 --- a/exercises/05.network/FINISHED.mdx +++ b/exercises/05.network/FINISHED.mdx @@ -1,5 +1,7 @@ # Network + + This concludes the network mocking exercise! :rocket: Now you know how to intercept requests in your tests and handle their responses. Whether it's responding with mock data or emulating error responses or network errors—you are in full control over the network when using MSW. diff --git a/exercises/05.network/README.mdx b/exercises/05.network/README.mdx index af7e706..d8726b4 100644 --- a/exercises/05.network/README.mdx +++ b/exercises/05.network/README.mdx @@ -1,5 +1,7 @@ # Network + + Network is the most common reason to introduce mocking to your test suite. Because that's also the most common thing you find in every application. You build products so they _do_ something, and on the web it takes the client and the server to achieve that. No wonder that many developers when told about mocking imagine API mocking, specifically. ## Why you should mock the network diff --git a/exercises/06.modules/01.problem.dependency-injection/README.mdx b/exercises/06.modules/01.problem.dependency-injection/README.mdx index 1968554..ec1783a 100644 --- a/exercises/06.modules/01.problem.dependency-injection/README.mdx +++ b/exercises/06.modules/01.problem.dependency-injection/README.mdx @@ -1,5 +1,7 @@ # Dependency injection + + First, let's see how you can use _dependency injection_ as an alternative to module mocking. We've already touched on dependency injection when mocking functions, but now, let's how a proper introduction to it. ## What is dependency injection? diff --git a/exercises/06.modules/01.solution.dependency-injection/README.mdx b/exercises/06.modules/01.solution.dependency-injection/README.mdx index 01b994d..59d5caa 100644 --- a/exercises/06.modules/01.solution.dependency-injection/README.mdx +++ b/exercises/06.modules/01.solution.dependency-injection/README.mdx @@ -1,5 +1,7 @@ # Dependency injection + + I will start by creating a fake `EmailService` class in the test: diff --git a/exercises/06.modules/02.problem.private-side-effects/README.mdx b/exercises/06.modules/02.problem.private-side-effects/README.mdx index 1f04525..b413bfd 100644 --- a/exercises/06.modules/02.problem.private-side-effects/README.mdx +++ b/exercises/06.modules/02.problem.private-side-effects/README.mdx @@ -1,5 +1,7 @@ # Private side effects + + There are cases when a third-party module may introduce root-level side effects that must be excluded during the test run. Take a look at the `authorize` function below. It accepts a `userId` and queries that user in a database, using a made-up `@workshop/epic-sdk` package. diff --git a/exercises/06.modules/02.solution.private-side-effects/README.mdx b/exercises/06.modules/02.solution.private-side-effects/README.mdx index 9ac0c3f..75bab80 100644 --- a/exercises/06.modules/02.solution.private-side-effects/README.mdx +++ b/exercises/06.modules/02.solution.private-side-effects/README.mdx @@ -1,5 +1,7 @@ # Private side effects + + As a rule of thumb, you should always strive toward mocking at the lowest possible level. Module mocking, however, lies on the higher spectrum of things. But that's not a bad thing in itself. In fact, if module mocking is justified, it gives you _full control over that module's values and behaviors_. I am going to utilize that power to both exclude the telemetry side effect and mock the implementation of the `queryTable` function exported from `@workshop/epic-sdk`. diff --git a/exercises/06.modules/FINISHED.mdx b/exercises/06.modules/FINISHED.mdx index 0118694..470893d 100644 --- a/exercises/06.modules/FINISHED.mdx +++ b/exercises/06.modules/FINISHED.mdx @@ -1,5 +1,7 @@ # Modules + + Good job on completing this exercise! :tada: Mocking modules is one of the most intrusive mocking techniques. It gives you a lot of power and control but keep in mind that with great power comes great responsibility. @@ -10,3 +12,4 @@ If you can, always prefer mocking specific values of behaviors before even consi - [Mocking modules in Vitest](https://vitest.dev/guide/mocking.html#modules) - [`vi.mock()`](https://vitest.dev/api/vi.html#vi-mock) +- [`vi.hoisted()`](https://vitest.dev/api/vi.html#vi-hoisted) diff --git a/exercises/06.modules/README.mdx b/exercises/06.modules/README.mdx index 4a02872..cb3449b 100644 --- a/exercises/06.modules/README.mdx +++ b/exercises/06.modules/README.mdx @@ -1,5 +1,7 @@ # Modules + + Mocking modules should be your last resort because it's the most intrusive mocking technique out there. As we've previously covered, mocking focuses on either _values_ or _behaviors_. When mocking a module, you are throwing away both, and leave it up to your mock to fill in the gaps in the best way possible. In practice, mocking modules may harm more than it may help. You rarely want to mock an _entire module_. You are problably thinking of a particular value or a behavior introduced by that module, and you should be mocking that instead, using the techniques you've already learned at this point. diff --git a/exercises/FINISHED.mdx b/exercises/FINISHED.mdx index 36badd1..1b23329 100644 --- a/exercises/FINISHED.mdx +++ b/exercises/FINISHED.mdx @@ -1,5 +1,7 @@ # Mocking Techniques + + Yoohoo! :tada: You've finished the workshop and have now officially become a mocking ninja 🥷. The techniques you've learned today will serve you well for the long years of testing to come. But the true power only comes with wisdom. Practice, learn, and hone your skills to perfection in that next test suite. What you have now are the building blocks, but it will be up to you to put them together, combine and experiment, to build a great testing experience at your next project. diff --git a/exercises/README.mdx b/exercises/README.mdx index 461752c..f31bee2 100644 --- a/exercises/README.mdx +++ b/exercises/README.mdx @@ -1,6 +1,8 @@ # Mocking Techniques -Hey! :wave: Welcome to the Mocking Techniques workshop! + + +Hey! :wave: Welcome to the Mocking Techniques in Vitest workshop! My name is Artem and over the course of the next hours I will make sure that you become a mocking ninja. It just so happened I am deeply involved in the developer tooling around mocking in JavaScript. I've built open source libraries to help developers with mocking that have become the standard of testing. I've learned a lot over the years of working on those projects, and it's finally time I helped you understand mocking. From f5d194a562a50acd9b91909cf60b8935d64ac050 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Fri, 11 Oct 2024 19:39:09 +0200 Subject: [PATCH 2/2] add ts-expect-error to intentionally invalid ts --- .../01.problem.dependency-injection/upload-service.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/exercises/06.modules/01.problem.dependency-injection/upload-service.test.ts b/exercises/06.modules/01.problem.dependency-injection/upload-service.test.ts index b4e6f55..b69d53b 100644 --- a/exercises/06.modules/01.problem.dependency-injection/upload-service.test.ts +++ b/exercises/06.modules/01.problem.dependency-injection/upload-service.test.ts @@ -1,6 +1,7 @@ import { FileStorage } from './file-storage.js' import { UploadService } from './upload-service.js' +// @ts-expect-error 💣 Remove this before proceeding. class FakeFileStorage implements FileStorage { // 🐨 In this `FakeFileStorage` class, declare a private variable called // `data`. Assign that variable to `new Map>`. @@ -26,6 +27,8 @@ test('stores a small file in a single chunk', async () => { // 🐨 Create a new variable called `storage` and assign it // to a new instance of `FakeFileStorage`. // 💰 const storage = value + + // @ts-expect-error 💣 Remove this before proceeding. const uploadService = new UploadService({ // 🐨 Provide the fake `storage` instance as the value // of the `storage` property to the `UploadService`. @@ -48,6 +51,8 @@ test('splits a large file in multiple chunks', async () => { // 🐨 Similarly, declare a `storage` variable and assign it // a new instance of the `FakeFileStorage` class. // 💰 const storage = new FakeFileStorage() + + // @ts-expect-error 💣 Remove this before proceeding. const uploadService = new UploadService({ // 🐨 Provide the fake `storage` as the option here. // 💰 storage,