|
| 1 | +## **Mocking & contracts** |
| 2 | + |
| 3 | +<br/> |
| 4 | + |
| 5 | +### ⚪️ 1. Identify good mocks from bad mocks |
| 6 | + |
| 7 | +🏷 **Tags:** `#basic, #strategic` |
| 8 | + |
| 9 | +:white_check_mark: **Do:** Mocking is neccessary evil we live with, in some cases they are just evil that will... often spoken about as one piece, it's useful to identify 3 types of by their purpose: |
| 10 | + |
| 11 | +isolation - prevent and check interactions with externous units like email send... This is done by stubbing a function that makes externous calls or by intercepting network requests. Isolation embodies two motivation: first avoid hitting external systems also for cost reasons - sending thousands emails a day will come with a bill. Second motivation is checking that our code is making the call in the right way. Calls to _external_ system is an effect, an outcome, of our code which should be tested for the same reason we test the code response. Inherently this mocks sit at the borders of the unit under test. This is not only legit but mandatory |
| 12 | + |
| 13 | +simulation - triggering some specific scenario that demands more than just an input like forcing an error or moving the time forward; Practically this means stubbing some function and triggering it to throw. While coupling the test to some internal mechanism or function is undesirable, it worth the risk if this an important scenario that can happen in production |
| 14 | + |
| 15 | +Implementation - This one is fundamentally different: It checks that that the code _internally_ worked as expected for example by checking that a in-scope function was invoked or a state is as expected. Practically this mocking style will spy-on or stub a function and assert that it was invoked as and when expected |
| 16 | + |
| 17 | +The key difference is in both isolation and simulation what the test check is external effects, outcomes and not implemenation. Implementation mocks are the only to check HOW the unit work and not WHAT it produces. This is the one that you want to avoid at any cose, a pure evil: it will fail when refactoring work even when the code is correct (false-positive) and will not warn when it shoud (false-negative), example here |
| 18 | + |
| 19 | +Note: It's not the mocking technique that tells a bad mock, both stubs and spies can be used for legit and mocking and also to assert on implemenation details |
| 20 | + |
| 21 | +Diagram unit under test borders vs internals?, 2x2, mocks differs by their purpose |
| 22 | + |
| 23 | +Grey black green, |
| 24 | + |
| 25 | +<br/> |
| 26 | + |
| 27 | +👀 **Alternatives:** one might spin the backend in Docker container or just a separate Node process. This configuration better resembles the production but it will lack critical testing features as mentioned above ❌; Some teams run integration tests against production-like cloud environment (see bullet 'Reuse tests against production-like environment), this is a valid technique for extra validation but will get too slow and limiting to rely on during development ❌; |
| 28 | + |
| 29 | +<br/> |
| 30 | + |
| 31 | +<details><summary>✏ <b>Code Examples</b></summary> |
| 32 | + |
| 33 | +```js |
| 34 | +const apiUnderTest = require('../api/start.js'); |
| 35 | + |
| 36 | +beforeAll(async () => { |
| 37 | + //Start the backend in the same process |
| 38 | +``` |
| 39 | +
|
| 40 | +➡️ [Full code here](https://github.com/testjavascript/integration-tests-a-z/blob/4c76cb2e2202e6c1184d1659bf1a2843db3044e4/example-application/entry-points/api-under-test.js#L10-L34) |
| 41 | +
|
| 42 | +</details> |
| 43 | +
|
| 44 | +<br/><br/> |
| 45 | +
|
| 46 | +### ⚪️ 2. Avoid hidden surprisng mocks |
| 47 | +
|
| 48 | +🏷 **Tags:** `#strategic` |
| 49 | +
|
| 50 | +:white_check_mark: **Do:** Mocks change the code and test behaviour, reader must be aware of they do. Put the mocks definition inside the test if they directly affect the test outcome, otherwise define in beforeEach |
| 51 | +
|
| 52 | +About big JSON, the dynamic factory |
| 53 | +
|
| 54 | +<br/> |
| 55 | +
|
| 56 | +👀 **Alternatives:** Jest mocks library; vitest something |
| 57 | +
|
| 58 | +<br/> |
| 59 | +
|
| 60 | +<details><summary>✏ <b>Code Examples</b></summary> |
| 61 | +
|
| 62 | +```js |
| 63 | +// dynamic factory |
| 64 | +``` |
| 65 | +
|
| 66 | +➡️ [Full code here](https://github.com/testjavascript/integration-tests-a-z/blob/4c76cb2e2202e6c1184d1659bf1a2843db3044e4/example-application/entry-points/api-under-test.js#L10-L34) |
| 67 | +
|
| 68 | +</details> |
| 69 | +
|
| 70 | +<br/><br/> |
| 71 | +
|
| 72 | +### ⚪️ 3. Prefer full-mocks over partial ones |
| 73 | +
|
| 74 | +🏷 **Tags:** `#advanced` |
| 75 | +
|
| 76 | +:white_check_mark: **Do:** When mocking we replace functions, having an object or class, should you replace one or all? Replacing one dangerous, a partial object - some mocked, some real, surprising effects and stability at risk. Code may hit other function and trigger charges |
| 77 | +
|
| 78 | +Alertnativelly, we can truncate the mocked object, assign some safe default to all functions (like throw) and the desired behaviour to a single function |
| 79 | +
|
| 80 | +When doing isolation type of mock, full-mock is desirable. We ensure no interaction with the external. |
| 81 | +
|
| 82 | +When doing simulation this rule skipped, we are forced to change implementation of internal object - changing all functions to the right behaviour is hard. We better take the risk of changing the one we need |
| 83 | +
|
| 84 | +<br/> |
| 85 | +
|
| 86 | +👀 **Alternatives:** Mocking a specific function of a class that interacts and incur charges might miss some other calls and result with charges |
| 87 | +
|
| 88 | +<br/> |
| 89 | +
|
| 90 | +<details><summary>✏ <b>Code Examples</b></summary> |
| 91 | +
|
| 92 | +```js |
| 93 | +// Set all functions to throw, change one function only |
| 94 | +``` |
| 95 | +
|
| 96 | +➡️ [Full code here](https://github.com/testjavascript/integration-tests-a-z/blob/4c76cb2e2202e6c1184d1659bf1a2843db3044e4/example-application/entry-points/api-under-test.js#L10-L34) |
| 97 | +
|
| 98 | +</details> |
| 99 | +
|
| 100 | +<br/><br/> |
| 101 | +
|
| 102 | +### ⚪️ 4. Clean-up all mocks BEFORE every test |
| 103 | +
|
| 104 | +🏷 **Tags:** `#advanced` |
| 105 | +
|
| 106 | +:white_check_mark: **Do:** |
| 107 | +
|
| 108 | +<br/> |
| 109 | +
|
| 110 | +👀 **Alternatives:** After each... |
| 111 | +
|
| 112 | +<br/> |
| 113 | +
|
| 114 | +<details><summary>✏ <b>Code Examples</b></summary> |
| 115 | +
|
| 116 | +```js |
| 117 | + |
| 118 | +``` |
| 119 | +
|
| 120 | +➡️ [Full code here](https://github.com/testjavascript/integration-tests-a-z/blob/4c76cb2e2202e6c1184d1659bf1a2843db3044e4/example-application/entry-points/api-under-test.js#L10-L34) |
| 121 | +
|
| 122 | +</details> |
| 123 | +
|
| 124 | +<br/><br/> |
| 125 | +
|
| 126 | +### ⚪️ 5. Be mindful about the mocking mechanism |
| 127 | +
|
| 128 | +🏷 **Tags:** `#advanced` |
| 129 | +
|
| 130 | +:white_check_mark: **Do:** There are signifcant differences between two main mocking techniques, each with its own consequences. Module based mocks like jest.mock works by changing the import/require behaviour while cache-based mocks just import an object and modify its behaviour relying on the fact that one single instance will exist |
| 131 | +
|
| 132 | +Module-based mocking comes with a price, it need to intercept the import so this must happen before the code and the test import the desired code. About bundling. It's path sensitive? |
| 133 | +
|
| 134 | +Cache-based mocking on the othr hand relies on another assumption - existence of object in-memory that both the test and sut act upon. What if the code under test destructures... |
| 135 | +
|
| 136 | +<br/> |
| 137 | +
|
| 138 | +👀 **Alternatives:** Mocking |
| 139 | +
|
| 140 | +<br/> |
| 141 | +
|
| 142 | +<details><summary>✏ <b>Code Examples</b></summary> |
| 143 | +
|
| 144 | +```js |
| 145 | + |
| 146 | +``` |
| 147 | +
|
| 148 | +➡️ [Full code here](https://github.com/testjavascript/integration-tests-a-z/blob/4c76cb2e2202e6c1184d1659bf1a2843db3044e4/example-application/entry-points/api-under-test.js#L10-L34) |
| 149 | +
|
| 150 | +</details> |
| 151 | +
|
| 152 | +<br/><br/> |
| 153 | +
|
| 154 | +### ⚪️ 6. Type your mocks |
| 155 | +
|
| 156 | +🏷 **Tags:** `#advanced` |
| 157 | +
|
| 158 | +:white_check_mark: **Do:** There are signifcant differences between two main mocking techniques, each with its own consequences. Module based mocks like jest.mock works by changing the import/require behaviour while cache-based mocks just import an object and modify its behaviour relying on the fact that one single instance will exist |
| 159 | +
|
| 160 | +Module-based mocking comes with a price, it need to intercept the import so this must happen before the code and the test import the desired code. About bundling. It's path sensitive? |
| 161 | +
|
| 162 | +Cache-based mocking on the othr hand relies on another assumption - existence of object in-memory that both the test and sut act upon. What if the code under test destructures... |
| 163 | +
|
| 164 | +<br/> |
| 165 | +
|
| 166 | +👀 **Alternatives:** Mocking |
| 167 | +
|
| 168 | +<br/> |
| 169 | +
|
| 170 | +<details><summary>✏ <b>Code Examples</b></summary> |
| 171 | +
|
| 172 | +```js |
| 173 | + |
| 174 | +``` |
| 175 | +
|
| 176 | +➡️ [Full code here](https://github.com/testjavascript/integration-tests-a-z/blob/4c76cb2e2202e6c1184d1659bf1a2843db3044e4/example-application/entry-points/api-under-test.js#L10-L34) |
| 177 | +
|
| 178 | +</details> |
0 commit comments