|
1 | 1 | ### Automock |
2 | 2 |
|
3 | | -Automock is a standalone library for unit testing. Using TypeScript Reflection |
4 | | -API (`reflect-metadata`) internally to produce mock objects, Automock streamlines |
5 | | -test development by automatically mocking class external dependencies. |
| 3 | +Automock is a powerful standalone library designed for unit testing. It leverages the TypeScript Reflection API |
| 4 | +internally to generate mock objects, simplifying the process of testing by automatically mocking external dependencies |
| 5 | +of classes. Automock enables you to streamline test development and focus on writing robust and efficient unit tests. |
| 6 | + |
6 | 7 | > info **info** `Automock` is a third party package and is not managed by the NestJS core team. |
7 | | -> Please, report any issues found with the library in the [appropriate repository](https://github.com/omermorad/automock) |
| 8 | +> Please, report any issues found with the library in the [appropriate repository](https://github.com/automock/automock) |
8 | 9 |
|
9 | 10 | #### Introduction |
10 | 11 |
|
11 | | -The dependency injection (DI) container is an essential component of the Nest module system. |
12 | | -This container is utilized both during testing, and the application execution. |
13 | | -Unit tests vary from other types of tests, such as integration tests, in that they must |
14 | | -fully override providers/services within the DI container. External class dependencies |
15 | | -(providers) of the so-called "unit", have to be totally isolated. That is, all dependencies |
16 | | -within the DI container should be replaced by mock objects. |
17 | | -As a result, loading the target module and replacing the providers inside it is a process |
18 | | -that loops back on itself. Automock tackles this issue by automatically mocking all the |
19 | | -class external providers, resulting in total isolation of the unit under test. |
| 12 | +The Dependency Injection (DI) container is a foundational element of the Nest module system, integral to both application |
| 13 | +runtime and testing phases. In unit tests, mock dependencies are essential for isolating and assessing the behavior of |
| 14 | +specific components. However, the manual configuration and management of these mock objects can be intricate and prone to |
| 15 | +errors. |
| 16 | + |
| 17 | +Automock offers a streamlined solution. Rather than interacting with the actual Nest DI container, Automock introduces a |
| 18 | +virtual container where dependencies are automatically mocked. This approach bypasses the manual task of substituting each |
| 19 | +provider in the DI container with mock implementations. With Automock, the generation of mock objects for all dependencies |
| 20 | +is automated, simplifying the unit test setup process. |
20 | 21 |
|
21 | 22 | #### Installation |
22 | 23 |
|
| 24 | +Automock support both Jest and Sinon. Just install the appropriate package for your testing framework of choice. |
| 25 | +Furthermore, you need to install the `@automock/adapters.nestjs` (as Automock supports other adapters). |
| 26 | + |
23 | 27 | ```bash |
24 | | -$ npm i -D @automock/jest |
| 28 | +npm i -D @automock/jest @automock/adapters.nestjs |
25 | 29 | ``` |
26 | 30 |
|
27 | | -Automock does not require any additional setup. |
| 31 | +Or, for Sinon: |
28 | 32 |
|
29 | | -> info **info** Jest is the only test framework currently supported by Automock. |
30 | | -Sinon will shortly be released. |
| 33 | +```bash |
| 34 | +npm i -D @automock/sinon @automock/adapters.nestjs |
| 35 | +``` |
31 | 36 |
|
32 | 37 | #### Example |
33 | 38 |
|
34 | | -Consider the following cats service, which takes three constructor parameters: |
| 39 | +The example provided here showcase the integration of Automock with Jest. However, the same principles |
| 40 | +and functionality applies for Sinon. |
| 41 | + |
| 42 | +Consider the following `CatService` class that depends on a `Database` class to fetch cats. We'll mock |
| 43 | +the `Database` class to test the `CatsService` class in isolation. |
35 | 44 |
|
36 | | -```ts |
37 | | -@@filename(cats.service) |
38 | | -import { Injectable } from '@nestjs/core'; |
| 45 | +```typescript |
| 46 | +@Injectable() |
| 47 | +export class Database { |
| 48 | + getCats(): Promise<Cat[]> { ... } |
| 49 | +} |
39 | 50 |
|
40 | 51 | @Injectable() |
41 | | -export class CatsService { |
42 | | - constructor( |
43 | | - private logger: Logger, |
44 | | - private httpService: HttpService, |
45 | | - private catsDal: CatsDal, |
46 | | - ) {} |
47 | | - |
48 | | - async getAllCats() { |
49 | | - const cats = await this.httpService.get('http://localhost:3000/api/cats'); |
50 | | - this.logger.log('Successfully fetched all cats'); |
51 | | - |
52 | | - this.catsDal.saveCats(cats); |
| 52 | +class CatsService { |
| 53 | + constructor(private database: Database) {} |
| 54 | + |
| 55 | + async getAllCats(): Promise<Cat[]> { |
| 56 | + return this.database.getCats(); |
53 | 57 | } |
54 | 58 | } |
55 | 59 | ``` |
56 | 60 |
|
57 | | -The service contains one public method, `getAllCats`, which is the method |
58 | | -we use an example for the following unit test: |
| 61 | +Let's set up a unit test for the `CatsService` class. |
| 62 | + |
| 63 | +We'll use the `TestBed` from the `@automock/jest` package to create our test environment. |
59 | 64 |
|
60 | | -```ts |
61 | | -@@filename(cats.service.spec) |
| 65 | +```typescript |
62 | 66 | import { TestBed } from '@automock/jest'; |
63 | | -import { CatsService } from './cats.service'; |
64 | 67 |
|
65 | | -describe('CatsService unit spec', () => { |
66 | | - let underTest: CatsService; |
67 | | - let logger: jest.Mocked<Logger>; |
68 | | - let httpService: jest.Mocked<HttpService>; |
69 | | - let catsDal: jest.Mocked<CatsDal>; |
70 | | - |
| 68 | +describe('Cats Service Unit Test', () => { |
| 69 | + let catsService: CatsService; |
| 70 | + let database: jest.Mocked<Database>; |
| 71 | + |
71 | 72 | beforeAll(() => { |
72 | | - const { unit, unitRef } = TestBed.create(CatsService) |
73 | | - .mock(HttpService) |
74 | | - .using({ get: jest.fn() }) |
75 | | - .mock(Logger) |
76 | | - .using({ log: jest.fn() }) |
77 | | - .mock(CatsDal) |
78 | | - .using({ saveCats: jest.fn() }) |
79 | | - .compile(); |
80 | | - |
81 | | - underTest = unit; |
82 | | - |
83 | | - logger = unitRef.get(Logger); |
84 | | - httpService = unitRef.get(HttpService); |
85 | | - catsDal = unitRef.get(CatsDal); |
| 73 | + const { unit, unitRef } = TestBed.create(CatsService).compile(); |
| 74 | + |
| 75 | + catsService = unit; |
| 76 | + database = unitRef.get(Database); |
86 | 77 | }); |
87 | 78 |
|
88 | | - describe('when getting all the cats', () => { |
89 | | - test('then meet some expectations', async () => { |
90 | | - httpService.get.mockResolvedValueOnce([{ id: 1, name: 'Catty' }]); |
91 | | - await catsService.getAllCats(); |
| 79 | + it('should retrieve cats from the database', async () => { |
| 80 | + const mockCats: Cat[] = [{ id: 1, name: 'Catty' }, { id: 2, name: 'Mitzy' }]; |
| 81 | + database.getCats.mockResolvedValue(mockCats); |
| 82 | + |
| 83 | + const cats = await catsService.getAllCats(); |
92 | 84 |
|
93 | | - expect(logger.log).toBeCalled(); |
94 | | - expect(catsDal).toBeCalledWith([{ id: 1, name: 'Catty' }]); |
95 | | - }); |
| 85 | + expect(database.getCats).toHaveBeenCalled(); |
| 86 | + expect(cats).toEqual(mockCats); |
96 | 87 | }); |
97 | 88 | }); |
98 | 89 | ``` |
99 | 90 |
|
100 | | -> info **info** The jest.Mocked<Source> utility type returns the Source type |
101 | | -> wrapped with type definitions of Jest mock function. ([reference](https://jestjs.io/docs/mock-function-api/#jestmockedsource)) |
| 91 | +In the test setup, we: |
| 92 | + |
| 93 | +1. Create a test environment for `CatsService` using `TestBed.create(CatsService).compile()`. |
| 94 | +2. Obtain the actual instance of `CatsService` and a mocked instance of `Database` using `unit` |
| 95 | + and `unitRef.get(Database)`, respectively. |
| 96 | +3. We mock the `getCats` method of the `Database` class to return a predefined list of cats. |
| 97 | +4. We then call the `getAllCats` method of `CatsService` and verify that it correctly interacts with the `Database` |
| 98 | + class and returns the expected cats. |
102 | 99 |
|
103 | | -#### About `unit` and `unitRef` |
| 100 | +**Adding a Logger** |
104 | 101 |
|
105 | | -Let's examine the following code: |
| 102 | +Let's extend our example by adding a `Logger` interface and integrating it into the `CatsService` class. |
106 | 103 |
|
107 | 104 | ```typescript |
108 | | -const { unit, unitRef } = TestBed.create(CatsService).compile(); |
| 105 | +@Injectable() |
| 106 | +class Logger { |
| 107 | + log(message: string): void { ... } |
| 108 | +} |
| 109 | + |
| 110 | +@Injectable() |
| 111 | +class CatsService { |
| 112 | + constructor(private database: Database, private logger: Logger) {} |
| 113 | + |
| 114 | + async getAllCats(): Promise<Cat[]> { |
| 115 | + this.logger.log('Fetching all cats..'); |
| 116 | + return this.database.getCats(); |
| 117 | + } |
| 118 | +} |
| 119 | +``` |
| 120 | + |
| 121 | +Now, when you set up your test, you'll also need to mock the `Logger` dependency: |
| 122 | + |
| 123 | +```typescript |
| 124 | +beforeAll(() => { |
| 125 | + let logger: jest.Mocked<Logger>; |
| 126 | + const { unit, unitRef } = TestBed.create(CatsService).compile(); |
| 127 | + |
| 128 | + catsService = unit; |
| 129 | + database = unitRef.get(Database); |
| 130 | + logger = unitRef.get(Logger); |
| 131 | +}); |
| 132 | + |
| 133 | +it('should log a message and retrieve cats from the database', async () => { |
| 134 | + const mockCats: Cat[] = [{ id: 1, name: 'Catty' }, { id: 2, name: 'Mitzy' }]; |
| 135 | + database.getCats.mockResolvedValue(mockCats); |
| 136 | + |
| 137 | + const cats = await catsService.getAllCats(); |
| 138 | + |
| 139 | + expect(logger.log).toHaveBeenCalledWith('Fetching all cats..'); |
| 140 | + expect(database.getCats).toHaveBeenCalled(); |
| 141 | + expect(cats).toEqual(mockCats); |
| 142 | +}); |
109 | 143 | ``` |
110 | 144 |
|
111 | | -Calling `.compile()` returns an object with two properties, `unit`, and `unitRef`. |
| 145 | +**Using `.mock().using()` for Mock Implementation** |
112 | 146 |
|
113 | | -**`unit`** is the unit under test, it is an actual instance of class being tested. |
| 147 | +Automock provides a more declarative way to specify mock implementations using the `.mock().using()` method chain. |
| 148 | +This allows you to define the mock behavior directly when setting up the `TestBed`. |
114 | 149 |
|
115 | | -**`unitRef`** is the "unit reference", where the mocked dependencies of the tested class |
116 | | -are stored, in a small container. The container's `.get()` method returns the mocked |
117 | | -dependency with all of its methods automatically stubbed (using `jest.fn()`): |
| 150 | +Here's how you can modify the test setup to use this approach: |
118 | 151 |
|
119 | 152 | ```typescript |
120 | | -const { unit, unitRef } = TestBed.create(CatsService).compile(); |
| 153 | +beforeAll(() => { |
| 154 | + const mockCats: Cat[] = [{ id: 1, name: 'Catty' }, { id: 2, name: 'Mitzy' }]; |
| 155 | + |
| 156 | + const { unit, unitRef } = TestBed.create(CatsService) |
| 157 | + .mock(Database) |
| 158 | + .using({ getCats: async () => mockCats }) |
| 159 | + .compile(); |
121 | 160 |
|
122 | | -let httpServiceMock: jest.Mocked<HttpService> = unitRef.get(HttpService); |
| 161 | + catsService = unit; |
| 162 | + database = unitRef.get(Database); |
| 163 | +}); |
123 | 164 | ``` |
124 | 165 |
|
125 | | -> info **info** The `.get()` method can accept either a `string` or an actual class (`Type`) as its argument. |
126 | | -> This essentially depends on how the provider is being injected to the class under test. |
| 166 | +In this approach, we've eliminated the need to manually mock the `getCats` method in the test body. |
| 167 | +Instead, we've defined the mock behavior directly in the test setup using `.mock().using()`. |
| 168 | + |
| 169 | +#### Dependency References and Instance Access |
| 170 | + |
| 171 | +When utilizing `TestBed`, the `compile()` method returns an object with two important properties: `unit` and `unitRef`. |
| 172 | +These properties provide access to the instance of the class under test and references to its dependencies, respectively. |
127 | 173 |
|
128 | | -#### Working with different providers |
129 | | -Providers are one of the most important elements in Nest. You can think of many of |
130 | | -the default Nest classes as providers, including services, repositories, factories, |
131 | | -helpers, and so on. A provider's primary function is to take the form of an |
| 174 | +`unit` - The unit property represents the actual instance of the class under test. In our example, it corresponds to an |
| 175 | +instance of the `CatsService` class. This allows you to directly interact with the class and invoke its methods during |
| 176 | +your test scenarios. |
| 177 | + |
| 178 | +`unitRef` - The unitRef property serves as a reference to the dependencies of the class under test. In our example, it |
| 179 | +refers to the `Logger` dependency used by the `CatsService`. By accessing `unitRef`, you can retrieve the automatically |
| 180 | +generated mock object for the dependency. This enables you to stub methods, define behaviors, and assert method |
| 181 | +invocations on the mock object. |
| 182 | + |
| 183 | +#### Working with Different Providers |
| 184 | + |
| 185 | +Providers are one of the most important elements in Nest. You can think of many of the default Nest classes as |
| 186 | +providers, including services, repositories, factories, helpers, and so on. A provider's primary function is to take the |
| 187 | +form of an |
132 | 188 | `Injectable` dependency. |
133 | 189 |
|
134 | | -Consider the following `CatsService`, it takes one parameter, which is an instance |
135 | | -of the following `Logger` interface: |
| 190 | +Consider the following `CatsService`, it takes one parameter, which is an instance of the following `Logger` interface: |
136 | 191 |
|
137 | 192 | ```typescript |
138 | 193 | export interface Logger { |
139 | 194 | log(message: string): void; |
140 | 195 | } |
141 | 196 |
|
| 197 | +@Injectable() |
142 | 198 | export class CatsService { |
143 | 199 | constructor(private logger: Logger) {} |
144 | 200 | } |
145 | 201 | ``` |
146 | 202 |
|
147 | | -TypeScript's Reflection API does not support interface reflection yet. |
148 | | -Nest solves this issue with string-based injection tokens (see [Custom Providers](https://docs.nestjs.com/fundamentals/custom-providers)): |
| 203 | +TypeScript's Reflection API does not support interface reflection yet. Nest solves this issue with string/symbol-based |
| 204 | +injection tokens (see [Custom Providers](https://docs.nestjs.com/fundamentals/custom-providers)): |
149 | 205 |
|
150 | 206 | ```typescript |
151 | 207 | export const MyLoggerProvider = { |
152 | | - provide: 'MY_LOGGER_TOKEN', |
| 208 | + provide: 'LOGGER_TOKEN', |
153 | 209 | useValue: { ... }, |
154 | 210 | } |
155 | 211 |
|
| 212 | +@Injectable() |
156 | 213 | export class CatsService { |
157 | | - constructor(@Inject('MY_LOGGER_TOKEN') private readonly logger: Logger) {} |
| 214 | + constructor(@Inject('LOGGER_TOKEN') readonly logger: Logger) {} |
158 | 215 | } |
159 | 216 | ``` |
160 | 217 |
|
161 | | -Automock follows this practice and lets you provide a string-based token instead |
162 | | -of providing the actual class in the `unitRef.get()` method: |
| 218 | +Automock follows this practice and lets you provide a string-based (or symbol-based) token instead of providing the actual |
| 219 | +class in the `unitRef.get()` method: |
163 | 220 |
|
164 | 221 | ```typescript |
165 | 222 | const { unit, unitRef } = TestBed.create(CatsService).compile(); |
166 | 223 |
|
167 | | -let loggerMock: jest.Mocked<Logger> = unitRef.get('MY_LOGGER_TOKEN'); |
| 224 | +let loggerMock: jest.Mocked<Logger> = unitRef.get('LOGGER_TOKEN'); |
168 | 225 | ``` |
169 | 226 |
|
170 | 227 | #### More Information |
171 | | -Visit [Automock GitHub repository](https://github.com/omermorad/automock) for more |
172 | | -information. |
| 228 | + |
| 229 | +Visit [Automock GitHub repository](https://github.com/automock/automock), or [Automock website](https://automock.dev) for more information. |
0 commit comments