@jbiskur/nestjs-test-utilities
v5.1.1
Published
The test utilities contain a set of builders that should speed up testing using method chaining and make the tests more descriptive.
Downloads
5,549
Readme
nestjs-test-utilities
The test utilities contain a set of builders that should speed up testing using method chaining and make the tests more descriptive.
Table of Contents
Installation
Install using npm.
npm install --save-dev @jbiskur/nestjs-test-utilities
using yarn.
yarn add --dev @jbiskur/nestjs-test-utilities
Usage
The library provides a set of builders that can be used to test various parts of the NestJS ecosystem.
Module
To facilitate the testing of a single module in isolation a module builder is provided that is a simple wrapper around the @nestjs/testing library.
import { TestModuleBuilder } from "@jbiskur/nestjs-test-utilities";
//... other imports
describe("Simple Example", () => {
it("should be get result from example service", async () => {
const module = await new TestModuleBuilder()
.withModule(TestModuleA)
.build()
.compile();
const serviceA = module.get(TestServiceA);
expect(await serviceA.helloFromA()).toBe("hello world");
});
});
withModule()
can be chained after each other to import additional modules, after the build()
method is called a normal TestingModule is returned, so methods like overrideProvider()
can be chained before the final compile()
command.
Application
A more useful feature of this library is the usage of the Application Builder. This creates a full NestJS application and initializes it so that all life-cycle methods are executed. Underneath the standard @nestjs/testing library is used.
import { INestApplication } from "@nestjs/common";
import {
NestApplicationBuilder,
} from "@jbiskur/nestjs-test-utilities";
//...import services and modules
describe("e2e test of module", () => {
// store the application in an easily accessible variable
let app: INestApplication;
afterEach(async () => {
// remember to close the application
await app.close();
});
it("module and service should be defined", async () => {
app = await new NestApplicationBuilder()
.withTestModule((builder) => builder.withModule(TestModuleA))
.build();
const testModuleA = app.get(TestModuleA);
const testServiceA = await app.resolve(TestServiceA);
expect(testModuleA).toBeDefined();
expect(testServiceA).toBeDefined();
});
}
the code above ensures that the full life-cycle methods in TestModuleA
are called.
A more complete example using overrides to mock data for specific services.
import { INestApplication } from "@nestjs/common";
import {
NestApplicationBuilder,
} from "@jbiskur/nestjs-test-utilities";
//...import services and modules
describe("Example overrides using jest Mock", () => {
// store the application in an easily accessible variable
let app: INestApplication;
const mockedOutput = "Hello from a mocked service";
let TestServiceMock: jest.Mock<TestServiceA, TestServiceA[]>;
beforeAll(() => {
TestServiceMock = jest
.fn<TestServiceA, TestServiceA[]>()
.mockImplementation(() => ({
helloFromA: () => mockedOutput,
}));
});
afterEach(async () => {
// remember to close the application
await app.close();
});
it("should work to override a provider using class", async () => {
app = await new NestApplicationBuilder()
.withTestModule((builder) => builder.withModule(TestModuleA))
.withOverrideProvider(TestServiceA, (overrideWith) =>
// useFactory and useValue are also supported
overrideWith.useClass(TestServiceMock)
)
.build();
const sut = await app.resolve(TestServiceA);
expect(sut.helloFromA()).toBe(mockedOutput);
});
}
A powerful way to simplify the testing in your apps or libraries, especially in a mono-repo configuration, is to extend the application builder to configure certain modules with a single method call for generic features such as database connections, graphql setup and loggers, as the example below illustrates.
import { GraphQLModule } from "@nestjs/graphql";
//...import logger
//...import typeorm
// first you extend the builder by extending the NestApplicationBuilder class
class ExtendedNestApplicationBuilder extends NestApplicationBuilder {
withGraphQLModule(): this {
this.withTestModule((builder) =>
builder.withModule(GraphQLModule.registerAsync({ autoSchemaFile: true }))
);
return this;
}
withSomeLoggingModule(): this {
this.withTestModule((builder) =>
builder.withModule(/* ...registerAsync, forRoot etc.. */)
);
return this;
}
withTypeORMConnection(): this {
this.withTestModule((builder) =>
builder.withModule(/* ...fx sql-lite in-memory database */)
);
return this;
}
}
this can then be use like the normal builder, but with the added functionality.
it("should work to extend the builder", async () => {
app = await new ExtendedNestApplicationBuilder()
.withTestModule((builder) => builder.withModule(TestModuleB))
.withGraphQLModule()
.withSomeLoggingModule()
.withTypeORMConnection()
.build();
const graphqlModule = await app.get(GraphQLModule);
const serviceB = await app.resolve(TestServiceB);
expect(graphqlModule).toBeDefined();
expect(serviceB.helloFromB()).toBe("hello world");
});
this way the tests are setup consistently throughout the test suite.
Application Instance
The last builder that is provided is a way to wrap the application builder in an instance builder. This creates a full server than can be queried on localhost.
import fetch from "cross-fetch";
import {
ApplicationInstance,
ApplicationInstanceBuilder,
NestApplicationBuilder,
} from "@jbiskur/nestjs-test-utilities";
import { INestApplication } from "@nestjs/common";
//...import modules etc..
describe("Application Server Instance", () => {
describe("listen on port 3000", () => {
let app: ApplicationInstance;
let instance: INestApplication;
const port = 3000;
beforeAll(async () => {
app = await new ApplicationInstanceBuilder(
new NestApplicationBuilder().withTestModule((builder) =>
builder.withModule(ModuleWithController)
)
).build(port);
instance = app.instance;
});
afterAll(async () => {
await instance.close();
});
it("should respond on port", async () => {
const result: Response = await fetch(`http://localhost:${app.port}/`);
expect(app.port).toBe(expected);
expect(await result.text()).toBe("hello world");
});
});
an example with overriding providers with moq.ts for mocking the result of a service
import { Mock } from "moq.ts";
//...setup, describe, it etc.
const mockedResponse = "Hello from a mocked ServiceA";
const MockedServiceA = new Mock<TestServiceA>()
.setup((instance) => instance.helloFromA())
.returns(mockedResponse);
beforeAll(async () => {
app = await new ApplicationInstanceBuilder(
new NestApplicationBuilder().withTestModule((builder) =>
builder.withModule(ModuleWithController)
)
.withOverrideProvider(TestServiceA, (overrideWith) =>
overrideWith.useValue(MockedServiceA.object())
);
).build();
instance = app.instance;
});
//... remember to close the instance
it("should respond with the mocked response", async () => {
const result: Response = await fetch(`http://localhost:${app.port}/`);
expect(await result.text()).toBe(mockedResponse);
});
using an extended builder this can be simplified even more if some service is constantly mocked. It can then be easy to mock generic services with a single method call.
class ExtendedNestApplicationBuilder extends NestApplicationBuilder {
// ...other extended methods
withOverriddenTestServiceA(): this {
this.withOverrideProvider(TestServiceA, (overrideWith) =>
overrideWith.useValue(MockedServiceA.object())
);
return this;
}
}
//... in test
beforeAll(async () => {
app = await new ApplicationInstanceBuilder(
new ExtendedNestApplicationBuilder()
.withTestModule((builder) => builder.withModule(ModuleWithController))
.withOverriddenTestServiceA()
).build();
instance = app.instance;
});
Plugins
the test builder supports plugins that can be used to share common builder patterns.
A plugin can be developed by extending the following interface
export interface INestApplicationBuilderPlugin {
run(appBuilder: NestApplicationBuilder): void;
}
An example graphql module plugin:
import { GraphQLModule } from "@nestjs/graphql";
class GraphQL implements INestApplicationBuilderPlugin {
private options: GraphQLOptions = {
autoSchemaFile: true,
};
withPlayground(): this {
options.playground = true;
return this;
}
withProduction(): this {
options.production = true;
}
// is executed last by the Application Builder
run(appBuilder: NestApplicationBuilder): void {
appBuilder.withTestModule((builder) =>
builder.withModule(GraphQLModule.registerAsync(this.options))
);
}
}
and using it with no options
app = await new NestApplicationBuilder()
.withTestModule((builder) => builder.withModule(TestModuleA))
.with(GraphQL)
.build();
with options
app = await new NestApplicationBuilder()
.withTestModule((builder) => builder.withModule(TestModuleA))
.with(GraphQL, (plugin) => plugin.withPlayground().withProduction())
.build();
When sharing NestApplicationBuilderPlugins on npm, please use the following naming convention
<name>-nestjs-builder-plugin
###Using it with another TestModuleBuilder for example using it to test nest-commander projects with nest-commander-testing.
first, add the packages
npm install --save nest-commander && npm install --save-dev nest-commander-testing
using yarn.
yarn add nest-commander && yarn add --dev nest-commander-testing
then implement your own builder like this:
import {
ITestModuleBuilder,
NestJSModule,
} from "@jbiskur/nestjs-test-utilities";
export class TestCommandBuilder implements ITestModuleBuilder {
private imports: NestJSModule[] = [];
private providers: Provider<unknown>[] = [];
build(): TestingModuleBuilder {
return CommandTestFactory.createTestingCommand({
imports: [...this.imports],
providers: [...this.providers],
});
}
withModule(nestModule: NestJSModule): ITestModuleBuilder {
this.imports.push(nestModule);
return this;
}
withProvider(provider: Provider<unknown>): ITestModuleBuilder {
this.providers.push(provider);
return this;
}
}
then to use it properly with nest-commander-testing extend the nestjs application builder
import { NestApplicationBuilder } from "@jbiskur/nestjs-test-utilities";
export class GTCommandInstanceBuilder extends NestApplicationBuilder<TestCommandBuilder> {
constructor() {
super(TestCommandBuilder);
}
async buildCommandInstance(): Promise<TestingModule> {
const testingModuleBuilder = await this.createTestingModule();
const testingModule = await testingModuleBuilder.compile();
return await testingModule.init();
}
}
it can then be used normally
commandInstance = await new GTCommandInstanceBuilder()
.withTestModule((builder) =>
builder.withModule({
imports: [AppModule],
})
)
.with(LogModulePlugin)
.withOverrideProvider(SomeService, (overrideWith) =>
overrideWith.useValue(someMockedService)
)
.buildCommandInstance();
Injecting
The test utilities has the ability to inject modules and providers into other modules during the build process. This is useful for testing modules that depend on other modules or injecting mocked modules and providers.
//...
app = await new NestApplicationBuilder()
.withTestModule((builder) => builder.withModule(TestModuleA))
.injectImports(TestModuleA, [SomeModuleB.forRoot({ /* some options */})])
.build();
//...
Override Module
The test utilities has the ability to override modules during the build process. This is useful for testing modules that depend on other modules or injecting mocked modules and providers.
//...
app = await new NestApplicationBuilder()
.withTestModule((builder) => builder.withModule(TestModuleA))
.overrideModule(TestModuleA, ModuleToOverride, MockedModule)
.build();
//...
Build As Microservice
The test utilities has the ability to build the application as a microservice. This is useful for testing microservices.
//...
app = await new NestApplicationBuilder()
.withTestModule((builder) => builder.withModule(TestModuleA))
.buildAsMicroservice({
transport: Transport.TCP,
options: {
host: 'localhost',
port: 3000,
},
});
//...
The
buildAsMicroservice
method returns aPromise<INestMicroservice>
and supports multiple microservice configurations by providing an array ofMicroserviceOptions
to the method.