|
| 1 | +# Mocking |
| 2 | +We've mentioned mocking in the last couple of lessons, but what exactly is mocking and how is it helpful in writing tests? The objectives of this lesson are: |
| 3 | +1. Understanding the use of mocking in tests |
| 4 | +2. Understanding different ways to implement mocking |
| 5 | + |
| 6 | +## What is mocking? |
| 7 | + |
| 8 | +Sometimes a function, class or component may be hard to test for because: |
| 9 | +- The class has dependencies which are hard to provide (e.g. the constructor expects multiple arguments) |
| 10 | +- Some functionality of a class may not be accessible from outside (e.g. by using access modifiers like private in TypeScript) |
| 11 | +- The function/class/component does far too much that we don't want in our tests |
| 12 | + |
| 13 | +In simple terms, mocking refers to replacing an actual piece of code with a simple piece of code that provides the required inputs by default. Mocking helps us write better tests because: |
| 14 | +- They eliminate functionality which we don't really want in the scope of a test. |
| 15 | +- Makes tests faster and less flaky by avoiding dependencies (e.g. access to a real database, using a third-party library) |
| 16 | +- Makes things easier to test. With mocking, we can easily create the ideal test setup to use in our tests. |
| 17 | +- Reduce the setup of our tests. Many libraries expect some setup to be done in order to work. With mocking, we can ignore all this and focus on testing actual functionality instead. |
| 18 | +- We don't need to test third-party code as it is probably already tested. In unit tests, we want to focus on the smaller parts of our application that we developed ourselves. Mocking third-party dependencies help to keep tests more focused on our custom logic and less about internal implementation details. |
| 19 | + |
| 20 | +## Monkey patching |
| 21 | + |
| 22 | +Monkey patching is a technique to add, modify, or suppress the default behavior of a piece of code at runtime without changing its original source code. It is a quick and easy way to implement mocking for tests. |
| 23 | + |
| 24 | +Let's look at an example of a controller that depends on a database model. |
| 25 | +```js |
| 26 | +async function getBook(req, res) { |
| 27 | + const book = await booksDB.readById(req.bookId) |
| 28 | + res.json({book}) |
| 29 | +} |
| 30 | +``` |
| 31 | +By looking at this code, we can guess that `booksDB` must be an object with one property `readById` whose value is a function: |
| 32 | +```js |
| 33 | +{ |
| 34 | + ... |
| 35 | + readbyId: function(id) { |
| 36 | + ... |
| 37 | + }, |
| 38 | + ... |
| 39 | +} |
| 40 | +``` |
| 41 | +The other model functions make up the rest of the key-value pairs in this object. |
| 42 | + |
| 43 | +In order to mock the database function, we can simply replace the specific `booksDB` function we are testing for with a determinisitc function. |
| 44 | + |
| 45 | +So, let's say we want to test for the case where booksDB returns `undefined` for some reason. In our test file, we can write: |
| 46 | +```js |
| 47 | +booksDB.readById = function(id) { |
| 48 | + return undefined |
| 49 | +} |
| 50 | +``` |
| 51 | + |
| 52 | +There we go, now when we execute the test on our controller it will get the value `undefined` when it calls the `booksDB.readById` function. In this way, we can make `readById` return a specific result to be used by our controller in each different test case. Such as returning a specific book or an error message or an incomplete book object. |
| 53 | + |
| 54 | +By using this technique, we are only testing how our controller handles different test cases and not testing the actual database model code which is a third-party. |
| 55 | + |
| 56 | +However, we did mention in the beginning that monkey patching works without changing the original source code, but we modified the code here. To make sure we don't completely modify the code, we can do this: |
| 57 | +```js |
| 58 | +// Keep track of original readById |
| 59 | +const originalReadById = booksDB.readById |
| 60 | +// Change temporarily to mock function |
| 61 | +booksDB.readById = function(id) { |
| 62 | + return undefined |
| 63 | +} |
| 64 | +// Run some test with mock readById |
| 65 | +... |
| 66 | +... |
| 67 | +... |
| 68 | +// Restore original readById |
| 69 | +booksDB.readById = originalReadById |
| 70 | +``` |
| 71 | + |
| 72 | +This ensures that the code is only temporarily modified to the mock version for the specific scope where it is required, and then it goes back to its original version to be used by other parts of the file. |
| 73 | + |
| 74 | +## Mocking in Jest |
| 75 | + |
| 76 | +Although monkey patching does the job, Jest provides more sophisticated methods for mocking. Going by the same example as above, we can mock the `readById` function using: |
| 77 | +```js |
| 78 | +// Keep track of original readById |
| 79 | +const originalReadById = booksDB.readById |
| 80 | +// Change temporarily to mock function |
| 81 | +booksDB.readById = jest.fn((id) => undefined) |
| 82 | +// Run some test with mock readById |
| 83 | +... |
| 84 | +... |
| 85 | +... |
| 86 | +// Restore original readById |
| 87 | +booksDB.readById = originalReadById |
| 88 | +``` |
| 89 | + |
| 90 | +`jest.fn()` creates a new mock function. But we can also use some Jest functions to track the original function and restore it. |
| 91 | +```js |
| 92 | +// Keep track of original readById |
| 93 | +jest.spyOn(booksDB, "readById") |
| 94 | +// Change temporarily to mock function |
| 95 | +booksDB.readById.mockImplementation((id) => undefined) |
| 96 | +// Run some test with mock readById |
| 97 | +... |
| 98 | +... |
| 99 | +... |
| 100 | +// Restore original readById |
| 101 | +booksDB.readById.mockRestore() |
| 102 | +``` |
| 103 | + |
| 104 | +The `spyOn`, `mockImplementation` and `mockRestore` functions help to track, implement mocking on and restore a function. |
| 105 | + |
| 106 | +Another advantage of using Jest mock functions, is to run assertions on the function calls: |
| 107 | +```js |
| 108 | +// The mock function was called at least once |
| 109 | +expect(mockFunc).toHaveBeenCalled(); |
| 110 | + |
| 111 | +// The mock function was called at least once with the specified args |
| 112 | +expect(mockFunc).toHaveBeenCalledWith(arg1, arg2); |
| 113 | + |
| 114 | +// The last call to the mock function was called with the specified args |
| 115 | +expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2); |
| 116 | +``` |
| 117 | + |
| 118 | +We can eliminate the actual functionality of third-party functions and focus simply on how, when and with what parameters was the function called. It helps us test our controllers' or services' interaction with the third-party functions. |
| 119 | + |
| 120 | +Using Jest, you can even mock entire modules. There are many different possiblities. Go through the Jest documentation for mocking [here](https://jestjs.io/docs/mock-functions) and [here](https://jestjs.io/docs/mock-function-api). |
| 121 | + |
| 122 | +## Mock Data |
| 123 | + |
| 124 | +Sometimes in our tests we may need large sets of mock data, such as mock names, mock addresses, mock phone numbers or mock credit card numbers depending on our application. It can be difficult to come up with such mock values but it's also not advisable to use too trivial values like "test", "123" or "lorem ipsum". |
| 125 | + |
| 126 | +In such cases, we can use libraries like [faker](https://www.npmjs.com/package/faker) which helps generate different kinds of fake/mock data which looks like real data. |
| 127 | + |
| 128 | +Some examples are: |
| 129 | +```js |
| 130 | +var faker = require('faker'); |
| 131 | +var randomName = faker.name.findName(); // Rowan Nikolaus |
| 132 | +var randomEmail = faker. internet. email(); // [email protected] |
| 133 | +var randomCard = faker.helpers.createCard(); // random contact card containing many properties |
| 134 | +``` |
| 135 | + |
| 136 | +It also has localization settings to generate fake data in many different languages. |
0 commit comments