@jbiskur/nestjs-test-utilities
TypeScript icon, indicating that this package has built-in type declarations

5.1.1 • Public • Published

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 a Promise<INestMicroservice> and supports multiple microservice configurations by providing an array of MicroserviceOptions to the method.

Readme

Keywords

none

Package Sidebar

Install

npm i @jbiskur/nestjs-test-utilities

Weekly Downloads

1,904

Version

5.1.1

License

MIT

Unpacked Size

56.7 kB

Total Files

58

Last publish

Collaborators

  • jbiskur
  • suuunly