Skip to content

Commit b059754

Browse files
committed
feat(testing): add auto-mocking capabilities
In the current `Test` module there isn't an easy way to say "If this dependency doesn't exist use this mock function instead." With this new `useMocker` fluent function it becomes possible to tell the `TestingInjector` to replace the undefined dependency with the automocking function. In doing so classes that have a large number of dependencies no longer must be mocked one-by-one . This is especially great when it comes to packages like `@golevelup/ts-jest` which can create mocked dependencies of interfaces and classes with a single function. I'm sure I've probably missed an edge case in here, so please let me know if you have any major concerns.
1 parent 95834b3 commit b059754

7 files changed

Lines changed: 139 additions & 3 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { FooService } from './foo.service';
3+
4+
@Injectable()
5+
export class BarService {
6+
7+
constructor(private readonly foo: FooService) {}
8+
9+
bar() {
10+
this.foo.foo();
11+
}
12+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Injectable } from '@nestjs/common';
2+
3+
@Injectable()
4+
export class FooService {
5+
foo() {
6+
console.log('foo called');
7+
}
8+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Test } from '@nestjs/testing';
2+
import { expect } from 'chai';
3+
import * as sinon from 'sinon';
4+
import { BarService } from '../src/bar.service';
5+
6+
describe('Auto-Mocking Bar Deps', () => {
7+
let service: BarService;
8+
let stub = sinon.stub();
9+
beforeEach(async () => {
10+
const moduleRef = await Test.createTestingModule({
11+
providers: [BarService],
12+
})
13+
.useMocker(() => ({ foo: stub }))
14+
.compile();
15+
service = moduleRef.get(BarService);
16+
});
17+
18+
it('should be defined', () => {
19+
expect(service).not.to.be.undefined;
20+
});
21+
it('should call bar.bar', () => {
22+
console.log(service);
23+
service.bar();
24+
expect(stub.called);
25+
});
26+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"compilerOptions": {
3+
"module": "commonjs",
4+
"declaration": false,
5+
"noImplicitAny": false,
6+
"removeComments": true,
7+
"noLib": false,
8+
"emitDecoratorMetadata": true,
9+
"experimentalDecorators": true,
10+
"target": "es6",
11+
"sourceMap": true,
12+
"allowJs": true,
13+
"outDir": "./dist"
14+
},
15+
"include": [
16+
"src/**/*",
17+
"e2e/**/*"
18+
],
19+
"exclude": [
20+
"node_modules",
21+
]
22+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {
2+
Injector,
3+
InjectorDependencyContext,
4+
} from '@nestjs/core/injector/injector';
5+
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
6+
import { Module } from '@nestjs/core/injector/module';
7+
import { STATIC_CONTEXT } from '@nestjs/core/injector/constants';
8+
import { Scope } from '@nestjs/common';
9+
10+
export class TestingInjector extends Injector {
11+
private mocker?: <T extends any = {}>() => T;
12+
setMocker(mocker: () => any): void {
13+
this.mocker = mocker;
14+
}
15+
16+
async resolveComponentInstance<T>(
17+
moduleRef: Module,
18+
name: any,
19+
dependencyContext: InjectorDependencyContext,
20+
wrapper: InstanceWrapper<T>,
21+
contextId = STATIC_CONTEXT,
22+
inquirer?: InstanceWrapper,
23+
keyOrIndex?: string | number,
24+
): Promise<InstanceWrapper> {
25+
try {
26+
const retWrapper = await super.resolveComponentInstance(
27+
moduleRef,
28+
name,
29+
dependencyContext,
30+
wrapper,
31+
contextId,
32+
inquirer,
33+
keyOrIndex,
34+
);
35+
return retWrapper;
36+
} catch (err) {
37+
if (this.mocker) {
38+
const newWrapper = new InstanceWrapper({
39+
name,
40+
isAlias: false,
41+
scope: wrapper.scope,
42+
instance: this.mocker<T>(),
43+
isResolved: true,
44+
});
45+
return newWrapper;
46+
} else {
47+
throw err;
48+
}
49+
}
50+
}
51+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { InstanceLoader } from '@nestjs/core/injector/instance-loader';
2+
import { TestingInjector } from './testing-injector';
3+
4+
export class TestingInstanceLoader extends InstanceLoader {
5+
protected injector = new TestingInjector();
6+
7+
async createInstancesOfDependencies(mocker?: () => any): Promise<void> {
8+
mocker && this.injector.setMocker(mocker);
9+
await super.createInstancesOfDependencies();
10+
}
11+
}

packages/testing/testing-module.builder.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,22 @@ import { Logger, LoggerService, Module } from '@nestjs/common';
22
import { ModuleMetadata } from '@nestjs/common/interfaces';
33
import { ApplicationConfig } from '@nestjs/core/application-config';
44
import { NestContainer } from '@nestjs/core/injector/container';
5-
import { InstanceLoader } from '@nestjs/core/injector/instance-loader';
65
import { MetadataScanner } from '@nestjs/core/metadata-scanner';
76
import { DependenciesScanner } from '@nestjs/core/scanner';
87
import { OverrideBy, OverrideByFactoryOptions } from './interfaces';
98
import { TestingLogger } from './services/testing-logger.service';
9+
import { TestingInstanceLoader } from './testing-instance-loader';
1010
import { TestingModule } from './testing-module';
1111

1212
export class TestingModuleBuilder {
1313
private readonly applicationConfig = new ApplicationConfig();
1414
private readonly container = new NestContainer(this.applicationConfig);
1515
private readonly overloadsMap = new Map();
1616
private readonly scanner: DependenciesScanner;
17-
private readonly instanceLoader = new InstanceLoader(this.container);
17+
private readonly instanceLoader = new TestingInstanceLoader(this.container);
1818
private readonly module: any;
1919
private testingLogger: LoggerService;
20+
private mocker?: () => any;
2021

2122
constructor(metadataScanner: MetadataScanner, metadata: ModuleMetadata) {
2223
this.scanner = new DependenciesScanner(
@@ -36,6 +37,11 @@ export class TestingModuleBuilder {
3637
return this.override(typeOrToken, false);
3738
}
3839

40+
public useMocker(mocker: () => any): TestingModuleBuilder {
41+
this.mocker = mocker;
42+
return this;
43+
}
44+
3945
public overrideFilter<T = any>(typeOrToken: T): OverrideBy {
4046
return this.override(typeOrToken, false);
4147
}
@@ -57,7 +63,7 @@ export class TestingModuleBuilder {
5763
await this.scanner.scan(this.module);
5864

5965
this.applyOverloadsMap();
60-
await this.instanceLoader.createInstancesOfDependencies();
66+
await this.instanceLoader.createInstancesOfDependencies(this.mocker);
6167
this.scanner.applyApplicationProviders();
6268

6369
const root = this.getRootModule();

0 commit comments

Comments
 (0)