|
| 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. |
0 commit comments