Skip to content

Commit 44dae8e

Browse files
[mabel] Add mocks and test pyramid
1 parent 51b6b30 commit 44dae8e

File tree

4 files changed

+363
-0
lines changed

4 files changed

+363
-0
lines changed

docs/_sidebar.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@
5656
- [Callback hell](javascript/callback-hell)
5757
- [Promises](javascript/promises)
5858
- [Async / Await](javascript/async-await)
59+
- [Test pyramid](javascript/test-pyramid)
5960
- [Jest](javascript/jest)
61+
- [Mocks](javascript/mocks)
6062
- [Labs for JavaScript](javascript/javascript-labs)
6163

6264
- **Frontend Web Development**

docs/backend-web-development/mongoose-testing.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Mongoose testing
22

3+
## Mock database vs real database
4+
5+
If we are writing unit tests, by definition we need to mock the interaction with database and we don't depend on a real database.
6+
7+
For integration test/contract test (e.g. on web service build using Express + MongoDB), we need to make the server up and running before we can send requests to it. Most of the time, that would require us to have a real database.
8+
9+
If somehow your tests need to depend on a real database, you need to make sure each test case has a clean database to start with. One solution is to set up and tear down all the collections in the database is necessary for ensuring there are no side effects between unit tests. In practice, this means a `beforeEach()` where you reconnect to the database and drop all collections, and an `afterEach()` where you disconnect from the database.
10+
11+
Another solution is to set up an **in-memory database** for each test case programmatically to avoid some of the issues with setting up a real database and sharing one database with all tests.
12+
313
## MongoDB memory server
414

515
To write API tests for an Express app that uses MongoDB as the database, we are going to use a library called **mongodb-memory-server**. It spins up an in-memory instance of MongoDB, which is faster than running a separate MongoDB instance.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Test pyramid
2+
3+
## Different types of testing
4+
5+
Unit Testing - validate that each unit of the software performs as expected
6+
7+
Integration Testing - ensure that the units work good together
8+
9+
Unit testing without integration testing:
10+
11+
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/mC3KO47tuG0" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
12+
13+
User Interface Testing (End-to-end (E2E) Testing) - test the application interfaces from the user's perspective
14+
15+
Regression Testing - verify that a previously developed and tested software hasn't regressed. (Snapshot testing is a kind of regression testing)
16+
17+
Manual testing - verify by hand that the software works/looks as you expect. This is time-consuming, non-repeatable and unscalable type of testing, and should be the least in proportion to the other types of tests
18+
19+
## More tests = better? no
20+
21+
On almost all of the projects, we have limited time and budget, and we don't have a lot of time to spend on writing and maintaining tests. So sometimes we need to make a choice on what kind of tests to write and maintain by considering the cost and benefits of those tests.
22+
23+
Also, tests take time to run. We want to be able to run tests quickly and often so that we can check that our code is passing the tests, especially in continuous integration.
24+
25+
The test pyramid is a model that helps illustrate this.
26+
27+
We should write the least number of tests that cover the most amount of code / functionality.
28+
29+
![software testing pyramid](https://gblobscdn.gitbook.com/assets%2F-LBJBL3Fj_tcfkvqLj9P%2F-LBJBWvA1rRm3yAXiRPh%2F-LBJBezpu28QsXc1lR9n%2Ftesting_pyramid.jpg?generation=1525052257824511&alt=media)
30+
31+
## Unit Test vs Integration Test / Contract Test
32+
33+
### Unit tests
34+
35+
Usually we prefer to have a lot of unit tests because they are easier to write, fast to run and easier to troubleshoot when they fail. However, one challenge of writing unit tests is to isolate the function/class under test from their environment.
36+
37+
Typically this requires us to mock the dependencies, but too many mock objects lead to its own problems.
38+
39+
Setting up mock objects for each test case could be tedious and makes the test cases harder to read.
40+
41+
Setting up mock objects sometimes requires knowledge on internal implementation of the functions under test (e.g. the implementation depends on some global object that is not passed as function arguments). If a test case is tightly coupled with the implementation, then we are forced to change the test case when implementation changes.
42+
43+
One way to address this concern is to follow dependency injection approach, by declaring all dependencies in the function arguments or constructor arguments.
44+
45+
### Integration tests
46+
47+
Usually we prefer to have fewer integration tests, especially those that depends on external systems (e.g. dependency on a real database or another web service). Because of those external dependencies, it could be difficult to setup the test environment, and the test cases may fail because of external reasons.
48+
49+
Having said that, one type of integration test for a web service is very helpful: the Contract Test. These test cases basically define how the APIs should behave under each scenario. Whenever we make new changes, as long as all contract tests pass, we can be pretty confident that the API consumers are not affected by the change.
50+
51+
When you build a system of multiple web services (e.g. using the micro-service architecture), it's critical that each service has its own API contract test so that every service can be released/upgraded independently and be confident that their changes do not break existing API consumers. In that scenario, the contract tests can also be written as Consumer Driven Contracts.

docs/javascript/mocks.md

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
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

Comments
 (0)