Skip to content

Commit 49ed013

Browse files
Merge pull request #2340 from omermorad/master
docs(recipes): add recipe for automock library
2 parents c222e17 + 0af008c commit 49ed013

File tree

4 files changed

+184
-0
lines changed

4 files changed

+184
-0
lines changed

content/recipes/automock.md

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
### Automock
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.
6+
> 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+
9+
#### Introduction
10+
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.
20+
21+
#### Installation
22+
23+
```bash
24+
$ npm i -D @automock/jest
25+
```
26+
27+
Automock does not require any additional setup.
28+
29+
> info **info** Jest is the only test framework currently supported by Automock.
30+
Sinon will shortly be released.
31+
32+
#### Example
33+
34+
Consider the following cats service, which takes three constructor parameters:
35+
36+
```ts
37+
@@filename(cats.service)
38+
import { Injectable } from '@nestjs/core';
39+
40+
@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);
53+
}
54+
}
55+
```
56+
57+
The service contains one public method, `getAllCats`, which is the method
58+
we use an example for the following unit test:
59+
60+
```ts
61+
@@filename(cats.service.spec)
62+
import { TestBed } from '@automock/jest';
63+
import { CatsService } from './cats.service';
64+
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+
71+
beforeAll(() => {
72+
const { unit, unitRef } = TestBed.create(CatsService)
73+
.mock(HttpService)
74+
.compile();
75+
76+
underTest = unit;
77+
78+
logger = unitRef.get(Logger);
79+
httpService = unitRef.get(HttpService);
80+
catsDal = unitRef.get(CatsDal);
81+
});
82+
83+
describe('when getting all the cats', () => {
84+
test('then meet some expectations', async () => {
85+
httpService.mockResolvedValueOnce([{ id: 1, name: 'Catty' }]);
86+
await catsService.getAllCats();
87+
88+
expect(logger.log).toBeCalled();
89+
expect(catsDal).toBeCalledWith([{ id: 1, name: 'Catty' }]);
90+
});
91+
});
92+
});
93+
```
94+
95+
> info **info** The jest.Mocked<Source> utility type returns the Source type
96+
> wrapped with type definitions of Jest mock function. ([reference](https://jestjs.io/docs/mock-function-api/#jestmockedsource))
97+
98+
#### About `unit` and `unitRef`
99+
100+
Let's examine the following code:
101+
102+
```typescript
103+
const { unit, unitRef } = TestBed.create(CatsService).compile();
104+
```
105+
106+
Calling `.compile()` returns an object with two properties, `unit`, and `unitRef`.
107+
108+
**`unit`** is the unit under test, it is an actual instance of class being tested.
109+
110+
**`unitRef`** is the "unit reference", where the mocked dependencies of the tested class
111+
are stored, in a small container. The container's `.get()` method returns the mocked
112+
dependency with all of its methods automatically stubbed (using `jest.fn()`):
113+
114+
```typescript
115+
const { unit, unitRef } = TestBed.create(CatsService).compile();
116+
117+
let httpServiceMock: jest.Mocked<HttpService> = unitRef.get(HttpService);
118+
```
119+
120+
> info **info** The `.get()` method can accept either a `string` or an actual class (`Type`) as its argument.
121+
> This essentially depends on how the provider is being injected to the class under test.
122+
123+
#### Working with different providers
124+
Providers are one of the most important elements in Nest. You can think of many of
125+
the default Nest classes as providers, including services, repositories, factories,
126+
helpers, and so on. A provider's primary function is to take the form of an
127+
`Injectable` dependency.
128+
129+
Consider the following `CatsService`, it takes one parameter, which is an instance
130+
of the following `Logger` interface:
131+
132+
```typescript
133+
export interface Logger {
134+
log(message: string): void;
135+
}
136+
137+
export class CatsService {
138+
constructor(private logger: Logger) {}
139+
}
140+
```
141+
142+
TypeScript's Reflection API does not support interface reflection yet.
143+
Nest solves this issue with string-based injection tokens (see [Custom Providers](https://docs.nestjs.com/fundamentals/custom-providers)):
144+
145+
```typescript
146+
export const MyLoggerProvider = {
147+
provide: 'MY_LOGGER_TOKEN',
148+
useValue: { ... },
149+
}
150+
151+
export class CatsService {
152+
constructor(@Inject('MY_LOGGER_TOKEN') private readonly logger: Logger) {}
153+
}
154+
```
155+
156+
Automock follows this practice and lets you provide a string-based token instead
157+
of providing the actual class in the `unitRef.get()` method:
158+
159+
```typescript
160+
const { unit, unitRef } = TestBed.create(CatsService).compile();
161+
162+
let loggerMock: jest.Mocked<Logger> = unitRef.get('MY_LOGGER_TOKEN');
163+
```
164+
165+
#### More Information
166+
Visit [Automock GitHub repository](https://github.com/omermorad/automock) for more
167+
information.

src/app/homepage/menu/menu.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ export class MenuComponent implements OnInit {
240240
{ title: 'Prisma', path: '/recipes/prisma' },
241241
{ title: 'Serve static', path: '/recipes/serve-static' },
242242
{ title: 'Commander', path: '/recipes/nest-commander' },
243+
{ title: 'Automock', path: '/recipes/automock' },
243244
],
244245
},
245246
{
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { ChangeDetectionStrategy, Component } from '@angular/core';
2+
import { BasePageComponent } from '../../page/page.component';
3+
4+
@Component({
5+
selector: 'app-automock',
6+
templateUrl: './automock.component.html',
7+
changeDetection: ChangeDetectionStrategy.OnPush,
8+
})
9+
export class AutomockComponent extends BasePageComponent {}

src/app/homepage/pages/recipes/recipes.module.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { SqlTypeormComponent } from './sql-typeorm/sql-typeorm.component';
1616
import { TerminusComponent } from './terminus/terminus.component';
1717
import { RouterModuleComponent } from './router-module/router-module.component';
1818
import { NestCommanderComponent } from './nest-commander/nest-commander.component';
19+
import { AutomockComponent } from './automock/automock.component';
1920

2021
const routes: Routes = [
2122
{
@@ -100,6 +101,11 @@ const routes: Routes = [
100101
component: ReplComponent,
101102
data: { title: 'REPL' },
102103
},
104+
{
105+
path: 'automock',
106+
component: AutomockComponent,
107+
data: { title: 'Automock' },
108+
},
103109
];
104110

105111
@NgModule({
@@ -118,6 +124,7 @@ const routes: Routes = [
118124
RouterModuleComponent,
119125
ServeStaticComponent,
120126
NestCommanderComponent,
127+
AutomockComponent,
121128
ReplComponent,
122129
],
123130
})

0 commit comments

Comments
 (0)