|
| 1 | +# Mocks |
| 2 | + |
| 3 | +Mocking is a technique to isolate test subjects by replacing dependencies with objects that you can control and inspect. |
| 4 | + |
| 5 | +The goal for mocking is to replace something we don’t control (e.g. a http request which may or may not succeed) with something we do. |
| 6 | + |
| 7 | +## What is a dependency? |
| 8 | + |
| 9 | +### Library Dependency |
| 10 | + |
| 11 | +Every system/module has its dependencies: it could be another module written by you (lib/math?), or it could be a module loaded from some 3rd party libraries. (npm?) |
| 12 | +Every time when you use `import` or `require` in your JavaScript code, you introduce a new dependency to the current JavaScript module. |
| 13 | + |
| 14 | +### Service Dependency |
| 15 | + |
| 16 | +If you write some frontend JavaScript codes that depends on another back-end API/web-service, you also introduce some dependency here. |
| 17 | + |
| 18 | +## Why do we need mocking? |
| 19 | + |
| 20 | +- Don't use mocking by default |
| 21 | + |
| 22 | +Mocking is a testing technique you should NOT use by default, because there are costs associated with mocks. |
| 23 | + |
| 24 | +You have less confidence that your code works with the real dependency that is mocked in your tests. This techniques violates the principles for [black box testing](http://softwaretestingfundamentals.com/black-box-testing/). The dependencies of the module under test is an implementation detail that your test is not supposed to know. |
| 25 | + |
| 26 | +- Setting up mocks requires additional codes that makes your tests more cluttered. |
| 27 | + |
| 28 | +So, only use mocks in a few scenarios where the mocking objects brings in more value than costs. A few cases are discussed below. |
| 29 | + |
| 30 | +### Use Case: Mocking dependencies to simulate various test scenarios |
| 31 | + |
| 32 | +When you try to test this system/module, sometimes you need to simulate some test scenarios where the dependencies need to behave in a controlled way. For this purpose, you need to mock the behavior of those modules/functions that your current module depend on. |
| 33 | + |
| 34 | +For example, we have a function below that generate some random arrays. It relies on the `randomInt` function from the `mathjs` module. If you need to test different scenarios, you need to control the return values from the randomInt function. But how can you do that? You can mock that randomInt function! |
| 35 | + |
| 36 | +```js |
| 37 | +const math = require("mathjs"); |
| 38 | + |
| 39 | +const generateQueue = () => { |
| 40 | + const randomInteger = math.randomInt(1, 10); |
| 41 | + const output = Array(randomInteger).map((number) => math.randomInt(-20, 50)); |
| 42 | + |
| 43 | + return output; |
| 44 | +}; |
| 45 | +``` |
| 46 | + |
| 47 | +### Use Case: Mocking dependencies to make test run faster and reliable |
| 48 | + |
| 49 | +If a test case depends on a real database instance as dependency, it may fail randomly (e.g. when the database server is not reachable for some reason), and it may run very slowly. |
| 50 | + |
| 51 | +To avoid those issues, we can mock the dependencies so that our test case do not depend on the status of real servers. |
| 52 | + |
| 53 | +### Use Case: Mocking Callbacks |
| 54 | + |
| 55 | +There are also cases when you need to pass a callback to the function you need to test, and you need to check if the callback is indeed invoked by the function. In this case, you can also pass a mock function as callback and verify it's called correctly. |
| 56 | + |
| 57 | +## Common patterns in a test case with mocked dependencies |
| 58 | + |
| 59 | +### Mocking dependencies with your own implementation |
| 60 | + |
| 61 | +When you write a test case with mock objects, the test case usually follow the steps below: |
| 62 | + |
| 63 | +- Mock the dependencies of system under test |
| 64 | +- Setup the system under test (SUT) (e.g create required object instances). Hook it up with mocked dependencies. |
| 65 | +- Call the API on the system under test. |
| 66 | +- Verify the result/behavior of the system under test |
| 67 | +- Verify the mocked dependencies are called as expected |
| 68 | + |
| 69 | +### Monitor the interaction between components with a spy |
| 70 | + |
| 71 | +Sometimes, you may not need to change the implementation/behavior of some function, you just want to keep an eye on it and check if it's called as expected. You can create a **spy** instead of a full mock. |
| 72 | + |
| 73 | +## Creating and using mock functions |
| 74 | + |
| 75 | +In order to mock a function, you use jest.fn(). |
| 76 | + |
| 77 | +### Creating a mock |
| 78 | + |
| 79 | +Creating a mock: `const myMockFunction = jest.fn()` |
| 80 | + |
| 81 | +Creating a mock with a name: `const mockFn = jest.fn().mockName('mockedFunction')` |
| 82 | + |
| 83 | +You can optionally provide a name for your mock functions, which will be displayed instead of "jest.fn()" in test error output. Use this if you want to be able to quickly identify the mock function reporting an error in your test output. |
| 84 | + |
| 85 | +### Verifying that the mock function has been called |
| 86 | + |
| 87 | +```js |
| 88 | +expect(myMockFunction).toBeCalled(); |
| 89 | +``` |
| 90 | + |
| 91 | +Checking the specific number of times that a mock function has been called: |
| 92 | + |
| 93 | +```js |
| 94 | +expect(myMockFunction).toHaveBeenCallTimes(2); |
| 95 | +``` |
| 96 | + |
| 97 | +### Verifying the arguments that were supplied to the mock |
| 98 | + |
| 99 | +```js |
| 100 | +expect(mockFunc).toBeCalledWith(arg1, arg2); |
| 101 | +``` |
| 102 | + |
| 103 | +### Stubbing a mock function's return value |
| 104 | + |
| 105 | +Make myMockFunction() return 42 everytime you call myMockFunction() |
| 106 | + |
| 107 | +```js |
| 108 | +const myMockFunction = jest.fn(() => 42); |
| 109 | +myMockFunction.mockReturnValue(42); |
| 110 | +``` |
| 111 | + |
| 112 | +Make myMockFunction() return this value only once (it returns undefined the next time it's called) |
| 113 | + |
| 114 | +```js |
| 115 | +myMockFunction.mockReturnValueOnce("you can return anything!"); |
| 116 | +``` |
| 117 | + |
| 118 | +### Replace the entire implementation of the function |
| 119 | + |
| 120 | +Replace the implementation of mockFn. |
| 121 | + |
| 122 | +```js |
| 123 | +const mockFn = jest.fn().mockImplementation((scalar) => 42 + scalar); |
| 124 | +// jest.fn(scalar => 42 + scalar); |
| 125 | + |
| 126 | +const a = mockFn(0); |
| 127 | +const b = mockFn(1); |
| 128 | + |
| 129 | +a === 42; // true |
| 130 | +b === 43; // true |
| 131 | +``` |
| 132 | + |
| 133 | +It is the same as: |
| 134 | + |
| 135 | +```js |
| 136 | +const mockFn = jest.fn((scalar) => 42 + scalar); |
| 137 | +``` |
| 138 | + |
| 139 | +### Sharing the same mock across test cases |
| 140 | + |
| 141 | +#### Clearing a mock |
| 142 | + |
| 143 | +Each mock functions keep track of various things (e.g. how many times it has been called). |
| 144 | + |
| 145 | +You'll need to clear those information beforeEach test case, so that each test case is kept independent. You can achieve this by calling `myMockFunction.mockClear()` in beforeEach. |
| 146 | + |
| 147 | +If you find yourself calling `.mockClear()` on multiple mocks, there is a command that let you clear all mocks in one line: `jest.clearAllMocks()`. |
| 148 | + |
| 149 | +#### Resetting a mock |
| 150 | + |
| 151 | +Sometimes, a mock function needs to behave differently in each test case, then you can call `myMockFunction.mockReset()` to remove the current mock return values / implementations. Then you can provide new mocked behavior. |
| 152 | + |
| 153 | +If you find yourself calling `.mockReset()` on multiple mocks, there is a command that let you reset all mocks in one line: `jest.resetAllMocks()`. |
| 154 | + |
| 155 | +Thus there is a difference between clearing and resetting. |
| 156 | + |
| 157 | +### Creating spies on existing functions |
| 158 | + |
| 159 | +In order to monitor the interaction between the system under tests and its dependencies (without mocking), you can use `jest.spyOn` to create a spy on the given function. After you finish testing, you can also call mockRestore to restore the original implementation. |
| 160 | + |
| 161 | +In the example below, you can put a spy on play function in the video and check it's called with your call to `playMovie` function. |
| 162 | + |
| 163 | +Example (in video.js) |
| 164 | + |
| 165 | +```js |
| 166 | +const video = { |
| 167 | + play() { |
| 168 | + return true; |
| 169 | + }, |
| 170 | +}; |
| 171 | + |
| 172 | +module.exports = video; |
| 173 | +``` |
| 174 | + |
| 175 | +Example (in mediaPlayer.js) |
| 176 | + |
| 177 | +```js |
| 178 | +const video = require("./video"); |
| 179 | + |
| 180 | +function playMovie() { |
| 181 | + return video.play(); |
| 182 | +} |
| 183 | + |
| 184 | +module.exports = { playMovie }; |
| 185 | +``` |
| 186 | + |
| 187 | +Example (in mediaPlayer.test.js) |
| 188 | + |
| 189 | +```js |
| 190 | +const video = require("./video"); |
| 191 | +const player = require("./moviePlayer"); |
| 192 | + |
| 193 | +test("plays video", () => { |
| 194 | + const spy = jest.spyOn(video, "play"); |
| 195 | + const isPlaying = player.playMovie(); |
| 196 | + |
| 197 | + expect(spy).toHaveBeenCalled(); |
| 198 | + expect(isPlaying).toBe(true); |
| 199 | + |
| 200 | + spy.mockRestore(); |
| 201 | +}); |
| 202 | +``` |
| 203 | + |
| 204 | +### Creating and using mock modules |
| 205 | + |
| 206 | +Besides mocking a function, you can also mock a whole JavaScript module with jest.mock() or jest.doMock(). |
| 207 | + |
| 208 | +Calls to jest.mock() will automatically be hoisted to the top of the code block |
| 209 | + |
| 210 | +In the example below, there are two modules: |
| 211 | + |
| 212 | +- someModule, a module written by you, which exports a function |
| 213 | +- mathjs, a module installed into node_modules, which exports a object with a function called randomInt |
| 214 | + |
| 215 | +anotherModule which internally calls require("./someModule") and require("mathjs") |
| 216 | + |
| 217 | +When you write tests for anotherModule, you may want to mock the behavior of someModule so that you can simulate different test scenarios. |
| 218 | + |
| 219 | +### Mocking a module written by you |
| 220 | + |
| 221 | +```js |
| 222 | +const myMockFunction = jest.fn(() => "dummy value"); |
| 223 | +jest.doMock("./someModule", () => { |
| 224 | + return myMockFunction; // Note: what you return here should match the exports in './someModule.js' |
| 225 | +}); |
| 226 | + |
| 227 | +// Note: it's crucial that you put this next line after jest.doMock() statements |
| 228 | +const anotherModule = require("./anotherModule.js"); |
| 229 | + |
| 230 | +/* |
| 231 | +now, inside anotherModule.js, when a line says `const x = require('./someModule'), |
| 232 | +x is the mock function returned from the factory function inside |
| 233 | +jest.doMock('./someModule', factoryFunction) |
| 234 | +*/ |
| 235 | +``` |
| 236 | + |
| 237 | +Alternative to doMock: |
| 238 | + |
| 239 | +```js |
| 240 | +jest.mock("./someModule", () => () => "dummy value"); |
| 241 | +``` |
| 242 | + |
| 243 | +### Mocking a module in node_modules |
| 244 | + |
| 245 | +```js |
| 246 | +jest.doMock("mathjs", () => { |
| 247 | + return { |
| 248 | + randomInt: () => 42, // always return 42 when math.randomInt() is called |
| 249 | + }; |
| 250 | +}); |
| 251 | + |
| 252 | +// Note: it's crucial that you put this next line after jest.doMock() statements |
| 253 | +const anotherModule = require("./anotherModule.js"); |
| 254 | + |
| 255 | +/* |
| 256 | +// inside anotherModule.js: |
| 257 | +
|
| 258 | +const math = require('mathjs') |
| 259 | +math.randomInt() // this will always return the stubbed value of 42 |
| 260 | +*/ |
| 261 | +``` |
| 262 | + |
| 263 | +### You can also put the mock implementations into a mocks directory |
| 264 | + |
| 265 | +Besides putting the mock implementation in the test case itself, you can also put some mock implementation in a directory **mocks** and inform jest to load that mock implementation when require is called to load the module. |
| 266 | + |
| 267 | +The benefit of this approach is that the mock implementation can be shared/reused by multiple test cases. |
| 268 | + |
| 269 | +More details can be found in Jest documentation on manual mocks. |
| 270 | + |
| 271 | +Putting it altogether |
| 272 | + |
| 273 | +Lab: https://github.com/thoughtworks-jumpstart/mocks-and-stubs-lab |
| 274 | + |
| 275 | +Solutions: https://github.com/songguoqiang/mocks-and-stubs-lab (don't peek unless you have to!) |
| 276 | + |
| 277 | +In the solutions repo, you can find examples on how to |
| 278 | + |
| 279 | +- Create mock functions: Using jest.fn() |
| 280 | +- Make mock functions return specific values (i.e. stubbing): Using myMockFunction.mockReturnValue('any value') or myMockFunction.mockReturnValueOnce('any value') |
| 281 | +- Make expectations/assertions on mock functions: Using expect(myMockFunction).toBeCalled() or expect(myMockFunction).toHaveBeenCalledTimes(42) |
| 282 | +- Clear mocks: Using myMockFunction.mockClear() or jest.clearAllMocks() |
| 283 | +- Mock functions imported from another module (i.e. javascript file or javascript library): Using |
| 284 | + |
| 285 | +```js |
| 286 | +jest.doMock("../src/queueService.js", () => { |
| 287 | + return mockGenerateQueue; |
| 288 | +}); |
| 289 | +``` |
| 290 | + |
| 291 | +or |
| 292 | + |
| 293 | +```js |
| 294 | +const mockRandomInt = jest.fn(); |
| 295 | +jest.doMock("mathjs", () => { |
| 296 | + return { |
| 297 | + randomInt: mockRandomInt, |
| 298 | + }; |
| 299 | +}); |
| 300 | +``` |
0 commit comments