Skip to content

Commit 560719b

Browse files
Merge pull request #2874 from omermorad/master
docs(recipes): update automock recipe for v2.0
2 parents 900b99d + 854eca8 commit 560719b

File tree

1 file changed

+151
-94
lines changed

1 file changed

+151
-94
lines changed

content/recipes/automock.md

Lines changed: 151 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,172 +1,229 @@
11
### Automock
22

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+
67
> 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)
89
910
#### Introduction
1011

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.
2021

2122
#### Installation
2223

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+
2327
```bash
24-
$ npm i -D @automock/jest
28+
$ npm i -D @automock/jest @automock/adapters.nestjs
2529
```
2630

27-
Automock does not require any additional setup.
31+
Or, for Sinon:
2832

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+
```
3136

3237
#### Example
3338

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.
3544

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+
}
3950

4051
@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();
5357
}
5458
}
5559
```
5660

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.
5964

60-
```ts
61-
@@filename(cats.service.spec)
65+
```typescript
6266
import { TestBed } from '@automock/jest';
63-
import { CatsService } from './cats.service';
6467

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+
7172
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);
8677
});
8778

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();
9284

93-
expect(logger.log).toBeCalled();
94-
expect(catsDal).toBeCalledWith([{ id: 1, name: 'Catty' }]);
95-
});
85+
expect(database.getCats).toHaveBeenCalled();
86+
expect(cats).toEqual(mockCats);
9687
});
9788
});
9889
```
9990

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.
10299

103-
#### About `unit` and `unitRef`
100+
**Adding a Logger**
104101

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.
106103

107104
```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+
});
109143
```
110144

111-
Calling `.compile()` returns an object with two properties, `unit`, and `unitRef`.
145+
**Using `.mock().using()` for Mock Implementation**
112146

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`.
114149

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:
118151

119152
```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();
121160

122-
let httpServiceMock: jest.Mocked<HttpService> = unitRef.get(HttpService);
161+
catsService = unit;
162+
database = unitRef.get(Database);
163+
});
123164
```
124165

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.
127173

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
132188
`Injectable` dependency.
133189

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:
136191

137192
```typescript
138193
export interface Logger {
139194
log(message: string): void;
140195
}
141196

197+
@Injectable()
142198
export class CatsService {
143199
constructor(private logger: Logger) {}
144200
}
145201
```
146202

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)):
149205

150206
```typescript
151207
export const MyLoggerProvider = {
152-
provide: 'MY_LOGGER_TOKEN',
208+
provide: 'LOGGER_TOKEN',
153209
useValue: { ... },
154210
}
155211

212+
@Injectable()
156213
export class CatsService {
157-
constructor(@Inject('MY_LOGGER_TOKEN') private readonly logger: Logger) {}
214+
constructor(@Inject('LOGGER_TOKEN') readonly logger: Logger) {}
158215
}
159216
```
160217

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:
163220

164221
```typescript
165222
const { unit, unitRef } = TestBed.create(CatsService).compile();
166223

167-
let loggerMock: jest.Mocked<Logger> = unitRef.get('MY_LOGGER_TOKEN');
224+
let loggerMock: jest.Mocked<Logger> = unitRef.get('LOGGER_TOKEN');
168225
```
169226

170227
#### 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

Comments
 (0)