Component Test Automation for NestJS Microservices

Hi all, I will share high-level information about how you can do component test automation for NestJS microservices in this tutorial. In another article, I will share how we can implement E2E (system integration/SIT) tests. First, I will start with which libraries we will use.

You should add the below libraries in your service’s package.json file. If you have a common project for all services that holds all the libraries, you can add the libraries below in that project’s package.json file.

  • chai → For assertions
  • crypto-js  → For random data generation.
  • jest → Test runner framework
  • supertest → API testing client library
  • allure-commandline →  Reporting library
  • jasmine →  Testing framework
  • jasmine-allure-reporter →  Reporting library
  • jest-allure →   Reporting library
  • jest-jasmine2 →  Testing framework (We need for allure reporting)
  • nock → Mocking library
  • pactum →  API testing client library (An alternative test automation library that is very promising.)

According to the library updates, the versions of these libraries will be increased. If any new library is needed, we should add them to the package.json file.

I want to share the high-level architecture of the component test automation solution.

nestjs component test auomation high level diagram

In the core library for the microservices, we will hold all common things and keep all libraries and their versions inside this library’s package.json file.

We keep the common things for the services in the test core library.

You can see the test core library structure in the screenshot below.

The folders inside this library are as follows:

builders: Common builders to build common objects like RequestObject.

models: Common DTO (model) objects like RequestObject.

services: Common services consumed by other services like authentication services.

utils: Common util classes are under this folder like SuperTestClient, HeaderUtil, RandomData util, Logging, etc.

I will share some of the important classes in this library.

Request Object DTO

It contains all the required parts of a request, like baserUrl, resource path, query parameters, headers, and payload.

Here the URL is formed by baseUrl + resourcePath. Like, http://localhost:8093/v1/carts 

Here, http://localhost:8093 is baseUrl, and /v1/carts  is the resourcePath.

export interface RequestObject {
  baseUrl?: string;
  resourcePath?: string;
  queryParams?: any;
  headers?: any;
  payload?: any;
}

Request Object Builder

The builder class builds request objects before hitting the services with them.

import { RequestObject } from '../models/request.object';

export class RequestObjectBuilder {
  private readonly _requestObject: RequestObject;

  constructor() {
    this._requestObject = {
      baseUrl: '',
      resourcePath: '',
      queryParams: '',
      headers: '',
      payload: '',
    };
  }

  baseUrl(baseUrl: string): this {
    this._requestObject.baseUrl = baseUrl;
    return this;
  }

  resourcePath(resourcePath: string): this {
    this._requestObject.resourcePath = resourcePath;
    return this;
  }

  queryParams(queryParams: any): this {
    this._requestObject.queryParams = queryParams;
    return this;
  }

  headers(headers: any): this {
    this._requestObject.headers = headers;
    return this;
  }

  payload(payload: any): this {
    this._requestObject.payload = payload;
    return this;
  }

  build(): RequestObject {
    return this._requestObject;
  }
}

Random Data Util

To create random data, I used the crypto library.

const { randomBytes } = require('node:crypto');

export class RandomDataUtil {
  /**
   * @description - generate random email
   */
  generateRandomEmail() {
    return `${randomBytes(10)
      .toString('base64')
      .replace(/[^a-zA-Z ]/g, '')}@email.com`;
  }

  /**
   * @description - generate random name
   */
  generateRandomName() {
    return `Test ${randomBytes(6)
      .toString('base64')
      .replace(/[^a-zA-Z ]/g, '')}`;
  }

  /**
   * @description - generate random password
   */
  generateRandomPassword() {
    return randomBytes(12).toString('base64');
  }

  /**
   * @description - generate random transaction ID
   */
  generateRandomTmxId() {
    const base = `${randomBytes(8)
      .toString('base64')
      .replace(/[^a-zA-Z0-9]/g, '')}-`.repeat(4);
    return base.substring(0, base.length - 1).toLowerCase();
  }
}

Header Util

The header util class contains methods we can use for header operations, as shown below.

export class HeaderUtil {
  headers: any;
  constructor() {
    this.headers = {
      Accept: 'application/json',
      'Content-Type': 'application/json',
    };
  }

  getHeaders(): any {
    return this.headers;
  }

  addHeader(header): HeaderUtil {
    this.headers = Object.assign(this.headers, header);
    return this;
  }

  removeHeader(key: string): HeaderUtil {
    delete this.headers[key];
    return this;
  }

  clearHeaders(): HeaderUtil {
    Object.keys(this.headers).forEach((key) => delete this.headers[key]);
    return this;
  }
}

Logging Util

This utility class writes the detailed logs when you enable it via the .dotenv config file from your tests. I will explain its usage in the test implementation section.

import { Response } from 'supertest';
import { Logger } from '@nestjs/common';

const logger = new Logger();
let isLoggingEnabled = true;

export class TestLogs {
  static enableLogs(logSwitch: boolean) {
    isLoggingEnabled = logSwitch;
  }

  static getLogSwitch() {
    return isLoggingEnabled;
  }

  static writeLogs(response: Response) {
    if (isLoggingEnabled) {
      const reqData = JSON.parse(JSON.stringify(response)).req;
      logger.log('Request-method : ' + JSON.stringify(reqData.method));
      logger.log('Request-url: ' + JSON.stringify(reqData.url));
      logger.log('Request-data: ' + JSON.stringify(reqData.data));
      logger.log('Request-headers: ' + JSON.stringify(reqData.headers));
      logger.log('Reponse-status: ' + JSON.stringify(response.status));
      logger.log('Reponse-headers: ' + JSON.stringify(response.headers));
      logger.log('Reponse-body: ' + JSON.stringify(response.body));
    }
  }
}

SuperTest Client

SuperTestClient class has the REST calls. We must send the Request object to hit the services using these methods.

import * as request from 'supertest';
import { RequestObject } from '../../models/request.object';
import { TestLogs } from '../logging/test.logs';

export class SuperTestClient {
  /**
   * @param requestObject - it contains all request data.
   * @description - GET request of the path resource
   * @response - Supertest response
   */
  async get(requestObject: RequestObject): Promise<request.Response> {
    const response = await request(requestObject.baseUrl)
      .get(requestObject.resourcePath)
      .query(requestObject.queryParams)
      .set(requestObject.headers)
      .send();
    TestLogs.writeLogs(response);
    return response;
  }

  /**
   * @param requestObject - it contains all request data.
   * @description - POST request of the path resource
   * @response - Supertest response
   */
  async post(requestObject: RequestObject): Promise<request.Response> {
    const response = await request(requestObject.baseUrl)
      .post(requestObject.resourcePath)
      .set(requestObject.headers)
      .send(requestObject.payload);
    TestLogs.writeLogs(response);
    return response;
  }

  /**
   * @param requestObject - it contains all request data.
   * @description - PUT request of the path resource
   * @response - Supertest response
   */
  async put(requestObject: RequestObject): Promise<request.Response> {
    const response = await request(requestObject.baseUrl)
      .put(requestObject.resourcePath)
      .set(requestObject.headers)
      .send(requestObject.payload);
    TestLogs.writeLogs(response);
    return response;
  }

  /**
   * @param requestObject - it contains all request data.
   * @description - PATCH request of the path resource
   * @response - Supertest response
   */
  async patch(requestObject: RequestObject) {
    const response = await request(requestObject.baseUrl)
      .patch(requestObject.resourcePath)
      .set(requestObject.headers)
      .send(requestObject.payload);
    TestLogs.writeLogs(response);
    return response;
  }

  /**
   * @param requestObject - it contains all request data.
   * @description - DELETE request of the path resource
   * @response - Supertest response
   */
  async delete(requestObject: RequestObject): Promise<request.Response> {
    const response = await request(requestObject.baseUrl)
      .delete(requestObject.resourcePath)
      .set(requestObject.headers)
      .send();
    TestLogs.writeLogs(response);
    return response;
  }
}

TS Config and Index.ts files

I will share the ts config and index.ts files below to reach the instance of these classes inside microservice project repositories.

import { HeaderUtil } from './utils/headers/header.util';
import { RandomDataUtil } from './utils/common/random.data.util';
import { SuperTestClient } from './utils/testclient/supertest.client';
import { RequestObjectBuilder } from './builders/request.object.builder';
import { TestLogs } from './utils/logging/test.logs';

export const TestCore = {
  HeaderUtil,
  RandomDataUtil,
  SuperTestClient,
  RequestObjectBuilder,
  TestLogs,
};

tsconfig.json

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es2020",
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "sourceMap": false,
    "incremental": false,
    "baseUrl": "./",
    "skipLibCheck": true,
    "strictNullChecks": false,
    "noImplicitAny": false,
    "strictBindCallApply": false,
    "forceConsistentCasingInFileNames": false,
    "noFallthroughCasesInSwitch": false,
    "declaration": true,
    "moduleResolution": "node"
  },
  "exclude": [
    "node_modules",
    "build",
    "dist",
    "**/*.spec.ts"
  ]
}

Component Tests Implementation in the Service Repositories

We created a ct folder under the test folder in the service repository and implemented component tests inside this folder. You can see the folder structure as follows:

Config

In the config folder, we are holding the test configurations under the ct folder; you can see the jest.config.ts file, which inherits the parent config file jest.config.ts under the config folder.

The below config file is the parent config file.

import type { Config } from 'jest';

const config: Config = {
  rootDir: './',
  moduleFileExtensions: ['js', 'json', 'ts'],
  transform: {
    '^.+\\.(t|j)s$': 'ts-jest',
  },
  moduleDirectories: ['node_modules'],
  preset: 'ts-jest',
  reporters: ['default', 'jest-allure'],
  testRunner: 'jest-jasmine2',
  testTimeout: 45000,
};

export default config;

And CT config file inherits the parent config file. Inside the ct config file, we added the root directory, test regex, and allure related settings, as shown below. You do not need to make any changes if it is required.

import sharedConfig from '../jest.config';

module.exports = {
  ...sharedConfig,
  testRegex: '.ct.spec.ts$',
  rootDir: '../../../',
  setupFilesAfterEnv: ['jest-allure/dist/setup'],
};

Builder

We have builder classes for the DTOs we use in component tests in the builder folder. As you see below, we used the builder pattern to build a DTO instance.

import { Cart } from '../models/cart';
import { v4 as uuidv4 } from 'uuid';
import { CartStatus } from '../typings/cart.status';

export class CartBuilder {
  private readonly _cart: Cart;

  constructor() {
    this._cart = {
      _type: 'Cart',
      description: 'CT shopping cart.',
      id: uuidv4(),
      product: 'Flight',
      status: CartStatus.OPEN,
    };
  }

  _type(_type: string): this {
    this._cart._type = _type;
    return this;
  }

  description(description: string): this {
    this._cart.description = description;
    return this;
  }

  id(id: string): this {
    this._cart.id = id;
    return this;
  }

  product(product: string): this {
    this._cart.product = product;
    return this;
  }

  status(status: CartStatus): this {
    this._cart.status = status;
    return this;
  }

  build(): Cart {
    return this._cart;
  }
}

Using the CartBuilder class, we can build a default or a specific cart instance using builder class methods, as shown in the below code snippet.

const cartBuilder = new CartBuilder();

const payload: Cart = cartBuilder
.product('Flight')
.description('Flight product')
.build();

Typings

In the typings folder, we hold constant values. It is like enum classes in JAVA. There is an example below.

export enum CartStatus {
  OPEN = 'OPEN',
  CLOSED = 'CLOSED',
}

Data

In the data folder, we hold data-related files like JSON files we use inside the tests, etc.

{
  "product": "Updated product.",
  "description": "Updated description."
}

Models

In the models folder, we hold the dto classes, as shown in the below code snippets.

export interface Cart {
  _type: string;
  description: string;
  id: string;
  product: string;
  status: string;
}
export interface Geo {
  continent: string;
  country: string;
  longitude: number;
  latitude: number;
}

Headers

In the CtHeaders class, we define the headers that we use in the component tests.

import { TestCore } from 'test-core';
import { Logger, Injectable } from '@nestjs/common';

@Injectable()
export class CtHeaders {
  private readonly logger = new Logger(CtHeaders.name);
  headerUtil = new TestCore.HeaderUtil();

  /**
   *  Headers for CT tests.
   */
  specificHeaders() {
    const headers = this.headerUtil.getHeaders();
    this.headerUtil.addHeader({
      specific_header_1: 'request-id-ct-1',
      specific_header_2: 'request-id-ct-2',
    });
    return headers;
  }
}

Note: If you have query parameters, you can use the same approach like headers.

Payloads

In the CtPayloads class, we define the headers that we use in the component tests.

import { Logger, Injectable } from '@nestjs/common';
import { Cart } from '../../models/cart';
import { CartBuilder } from '../../build/cart.builder';
import updateCartJson = require('../data/update.cart.json');

const cartBuilder = new CartBuilder();

@Injectable()
export class CtPayloads {
  private readonly logger = new Logger(CtPayloads.name);

  flightPayload() {
    const payload: Cart = cartBuilder
      .product('Flight Product for CT tests.')
      .description('Flight product for CT tests.')
      .build();
    return payload;
  }

  customPayload(product: string, description: string) {
    const payload: Cart = cartBuilder
      .product(product)
      .description(description)
      .build();
    return payload;
  }

  updateCartPayload() {
    return JSON.stringify(updateCartJson);
  }
}

Mocks

In the Mocks class, we define the mocks that we use in the component tests. For mocking, I used the nock library.

import nock = require('nock');

export class Mocks {
  async nockGeoDs() {
    return await nock('https://www.swtestacademy.com')
      .get('/service/geo')
      .reply(200, {
        continent: 'as',
        country: 'ae',
        longitude: 55.28,
        latitude: 25.25,
      });
  }
}

Service

I implement common scenarios in the service class. It is shown in the below example.

import { TestCore } from 'test-core';
import { Logger, Injectable } from '@nestjs/common';
import { Response } from 'supertest';
import { CartBuilder } from '../build/cart.builder';
import { Cart } from '../models/cart';

@Injectable()
export class SkeletonService {
  private readonly logger = new Logger(SkeletonService.name);
  headerUtil = new TestCore.HeaderUtil();
  superTestClient = new TestCore.SuperTestClient();
  cartBuilder = new CartBuilder();

  /**
   * Common Scenarios can be used in the tests.
   * If this service consumed by other services, then it is nice to
   * move this implementation to the test core library. In this way,
   * we will not re-implement this service content for each service repository.
   */

  async createACart(
    baseUrl: string,
    resourcePath: string,
    headers: any,
  ): Promise<Response> {
    this.logger.log('Creating a cart!');

    const payload: Cart = this.cartBuilder
      .product('Ticket')
      .description('Ticket product')
      .build();

    return await this.superTestClient.post(
      new TestCore.RequestObjectBuilder()
        .baseUrl(baseUrl)
        .resourcePath(resourcePath)
        .headers(headers)
        .payload(payload)
        .build(),
    );
  }
}

Steps

We write the tests with Gherkin fashion, so I create Given, When, Then, and Steps classes. In the tests, we use the steps class instance to reach all methods of all classes. In this way, we can fluently write the tests.

Given Class

In the Given class, we prepare our requests and do prerequisite operations based on our service details and business requirements.

import { CtHeaders } from '../headers/ct.headers';
import { appServer, requestObjectBuilder } from '../specs/base.spec';
import { SkeletonService } from '../../service/skeleton.service';
import { Cart } from 'test/models/cart';
import { Logger } from '@nestjs/common';

declare const reporter: any;
const skeletonService = new SkeletonService();
const ctHeaders = new CtHeaders();

export abstract class Given<T extends Given<T>> {
  private readonly loggerGiven = new Logger(Given.name);
  createCartResponse: any;
  createCartResponseBody: Cart;

  async givenIHaveARequestWithPayload(
    resourcePath: string,
    headers: any,
    payload: any,
  ): Promise<T> {
    await reporter.startStep('Preparing the request for creating a cart.');
    requestObjectBuilder
      .resourcePath(resourcePath)
      .headers(headers)
      .payload(payload);
    await reporter.endStep();
    return this.getThis();
  }

  async givenIHaveARequest(resourcePath: string, headers: any): Promise<T> {
    await reporter.startStep('Preparing the request for creating a cart.');
    requestObjectBuilder.resourcePath(resourcePath).headers(headers);
    await reporter.endStep();
    return this.getThis();
  }

  async givenIHaveCarts(): Promise<T> {
    await reporter.startStep('Creating carts for the tests.');
    await skeletonService.createACart(
      appServer,
      '/carts',
      ctHeaders.headerUtil.getHeaders(),
    );
    await skeletonService.createACart(
      appServer,
      '/carts',
      ctHeaders.headerUtil.getHeaders(),
    );
    await reporter.endStep();
    return this.getThis();
  }

  async givenIHaveCart(): Promise<T> {
    await reporter.startStep('Creating a cart for the tests.');
    this.createCartResponse = await skeletonService.createACart(
      appServer,
      '/carts',
      ctHeaders.headerUtil.getHeaders(),
    );
    this.createCartResponseBody = this.createCartResponse.body;
    await reporter.endStep();
    return this.getThis();
  }

  protected abstract getThis(): T;
}

When Class

In the When class, we send requests to our service in the When class by using GET, POST, PUT, and DELETE method calls from test-core’s supertestclient methods.

import { Logger } from '@nestjs/common';
import { Given } from './given';
import { TestCore } from 'test-core';
import { requestObject } from '../specs/base.spec';
import { Response } from 'supertest';

declare const reporter: any;
const superTestClient = new TestCore.SuperTestClient();

export abstract class When<T extends When<T>> extends Given<T> {
  private readonly loggerWhen = new Logger(When.name);
  protected response: Response;

  async whenIPost(): Promise<T> {
    await reporter.startStep('Hitting the service endpoint to create a cart.');
    this.response = await superTestClient.post(requestObject);
    await reporter.endStep();
    return this.getThis();
  }

  async whenIGet(): Promise<T> {
    await reporter.startStep('Hitting the service endpoint to get cart/s.');
    this.loggerWhen.log('Getting the cart/s.');
    this.response = await superTestClient.get(requestObject);
    await reporter.endStep();
    return this.getThis();
  }

  async whenIPatch(): Promise<T> {
    await reporter.startStep(
      'Hitting the service endpoint to modifying cart/s.',
    );
    this.loggerWhen.log('Modifying the cart/s.');
    this.response = await superTestClient.patch(requestObject);
    await reporter.endStep();
    return this.getThis();
  }

  async whenIDelete(): Promise<T> {
    await reporter.startStep(
      'Hitting the service endpoint to deleting cart/s.',
    );
    this.loggerWhen.log('Deleting the cart/s.');
    this.response = await superTestClient.delete(requestObject);
    await reporter.endStep();
    return this.getThis();
  }
}

Then Class

In the Then class, we do assertions and verifications, such as status checks and response assertions based on business requirements.

import { Logger } from '@nestjs/common';
import { When } from './when';
import { Cart } from '../../models/cart';
import { expect } from 'chai';
import { Geo } from 'test/models/geo';

declare const reporter: any;

export abstract class Then<T extends Then<T>> extends When<T> {
  private readonly loggerThen = new Logger(Then.name);

  async thenIShouldVerifyDownStreamResponse(): Promise<T> {
    await reporter.startStep('Verfiy that the downstream response.');
    const responseBody: Geo = this.response.body;
    expect(this.response.status).to.be.equal(200);
    expect(responseBody.continent).to.be.equal('as');
    await reporter.endStep();
    return this.getThis();
  }

  async thenIShouldSeeCartCreatedSuccessfully(): Promise<T> {
    await reporter.startStep('Verfiy that the cart has been created.');
    const responseBody: Cart = this.response.body;
    this.loggerThen.log('Create Cart Res: ' + JSON.stringify(responseBody));
    expect(this.response.status).to.be.equal(201);
    expect(responseBody.description).to.be.contains('Flight product');
    expect(responseBody.product).to.be.equal('Flight Product for CT tests.');
    await reporter.endStep();
    return this.getThis();
  }

  async thenIShouldGetCartsSuccessfully(): Promise<T> {
    await reporter.startStep('Verfiy that the carts have been retrieved.');
    const responseBody: Array<Cart> = this.response.body;
    this.loggerThen.log('Get Carts Res: ' + JSON.stringify(responseBody));
    expect(this.response.status).to.be.equal(200);
    expect(responseBody).to.be.an('array').length.greaterThan(0);
    await reporter.endStep();
    return this.getThis();
  }

  async thenIShouldGetCartSuccessfully(): Promise<T> {
    await reporter.startStep('Verfiy that the cart have been retrieved.');
    const responseBody: Cart = this.response.body;
    this.loggerThen.log('Get Cart Res: ' + JSON.stringify(responseBody));
    expect(this.response.status).to.be.equal(200);
    expect(responseBody.product).to.be.equal(
      this.createCartResponseBody.product,
    );
    expect(responseBody.description).to.be.equal(
      this.createCartResponseBody.description,
    );
    expect(responseBody.status).to.be.equal(this.createCartResponseBody.status);
    expect(responseBody._type).to.be.equal(this.createCartResponseBody._type);
    await reporter.endStep();
    return this.getThis();
  }

  async thenIShouldSeeCartModifiedSuccessfully(): Promise<T> {
    await reporter.startStep('Verfiy that the cart have been modified.');
    const responseBody: Cart = this.response.body;
    this.loggerThen.log('Patch Cart Res: ' + JSON.stringify(responseBody));
    expect(this.response.status).to.be.equal(200);
    expect(responseBody.description).to.be.contains('Updated description.');
    expect(responseBody.product).to.be.equal('Updated product.');
    await reporter.endStep();
    return this.getThis();
  }

  async thenIShouldSeeCartDeletedSuccessfully(): Promise<T> {
    await reporter.startStep('Verfiy that the cart have been deleted.');
    const responseBody: Cart = this.response.body;
    this.loggerThen.log('Delete Cart Res: ' + JSON.stringify(responseBody));
    expect(this.response.status).to.be.equal(200);
    expect(this.response.body.cas).not.to.be.empty;
    await reporter.endStep();
    return this.getThis();
  }
}

Steps Class

In the Steps class, we can reach all Given, When, and Then classes methods.

import { Logger } from '@nestjs/common';
import { Then } from './then';

export class Steps extends Then<Steps> {
  private readonly loggerSteps = new Logger(Steps.name);
  protected getThis(): Steps {
    return this;
  }
}

Specs

Specs are our tests in the NestJS project. We locate the test classes inside the specs folder. The test classes inherit the base.spec.ts class. In the base.spec.ts class, we hold the common stuff we use in the test classes.

base.spec.ts

Inside the beforeEach block, we set up the allure reporter, and inside the beforeAll block, we start the Application on localhost. Also, we initiate the stubs in this block too. For your project requirements, please check here: https://docs.nestjs.com/fundamentals/testing

import { Test, TestingModule } from '@nestjs/testing';
import {
  FastifyAdapter,
  NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { start } from 'ottoman';
import { AppModule } from '../../../src/app.module';
import { Mocks } from '../mocks/mocsk';
import { TestCore } from 'test-core';
import { RequestObject } from 'test-core/model/request.object';
import { Logger } from '@nestjs/common';

const { ENABLE_TEST_LOGS, PORT } = process.env;
const enableLogs = ENABLE_TEST_LOGS === 'true' ? true : false;
const port = Number(PORT);
let app: NestFastifyApplication;
let appServer: any;
declare const reporter: any;
const mocks = new Mocks();
const requestObjectBuilder = new TestCore.RequestObjectBuilder();
let requestObject: RequestObject;

beforeEach(async () => {
  reporter
    .epic('Component Tests.')
    .feature('Shopping Cart Feature.')
    .story('Cart Crud Story.')
    .description(expect.getState().currentTestName);

  //BaseUrl is same for the all tests. That's why it is defined here to remove code duplications.
  requestObject = requestObjectBuilder.baseUrl(appServer).build();
});

beforeAll(async () => {
  const moduleRef: TestingModule = await Test.createTestingModule({
    imports: [AppModule],
  }).compile();
  app = moduleRef.createNestApplication<NestFastifyApplication>(
    new FastifyAdapter(),
  );
  await app.init();
  await app.listen(port); //For random ports: await app.listen(0);
  appServer = app.getHttpServer();
  if (enableLogs) app.useLogger(new Logger());
  await mocks.nockGeoDs();
  await start(); //Creates Couchbase indexes if not there
});

afterAll(async () => {
  await app.close();
});

export { appServer, reporter, requestObjectBuilder, requestObject };

carts.ct.step.ts

This is our test class, and inside describe block, we specify our tests by using it keyword. We follow given-when-then approach in our tests. 

In the Given part, we prepare the test data and do prerequisite operations.

In the When part, we hit the service with the test client instance and the prepared request object instance.

In the Then part, we do the assertions and verifications.

import { Steps } from '../steps/steps';
import { CtHeaders } from '../headers/ct.headers';
import { CtPayloads } from '../payloads/ct.payloads';
const steps = new Steps();
const ctHeaders = new CtHeaders();
const ctPayloads = new CtPayloads();
describe('Shopping Cart Component Tests.', () => {
  it('Carts Geo Test - Downstream Mocking Example.', async () => {
    await steps.givenIHaveARequest(
      '/carts/ds/geo',
      ctHeaders.headerUtil.getHeaders,
    );
    await steps.whenIGet();
    await steps.thenIShouldVerifyDownStreamResponse();
  });
  it('Get carts.', async () => {
    await steps.givenIHaveCarts();
    await steps.givenIHaveARequest('/carts', ctHeaders.headerUtil.getHeaders);

    await steps.whenIGet();
    await steps.thenIShouldGetCartsSuccessfully();
  });
  it('Create a cart', async () => {
    await steps.givenIHaveARequestWithPayload(
      '/carts',
      ctHeaders.headerUtil.getHeaders(),
      ctPayloads.flightPayload(),
    );
    await steps.whenIPost();
    await steps.thenIShouldSeeCartCreatedSuccessfully();
  });
  it('Get a single cart', async () => {
    await steps.givenIHaveCart();
    await steps.givenIHaveARequest(
      '/carts' + '/' + steps.createCartResponseBody.id,
      ctHeaders.headerUtil.getHeaders(),
    );
    await steps.whenIGet();
    await steps.thenIShouldGetCartSuccessfully();
  });
  it('Update a cart', async () => {
    await steps.givenIHaveCart();
    await steps.givenIHaveARequestWithPayload(
      '/carts' + '/' + steps.createCartResponseBody.id,
      ctHeaders.headerUtil.getHeaders(),
      ctPayloads.updateCartPayload(),
    );
    await steps.whenIPatch();
    await steps.thenIShouldSeeCartModifiedSuccessfully();
  });
  it('Delete a single cart', async () => {
    await steps.givenIHaveCart();
    await steps.givenIHaveARequest(
      '/carts' + '/' + steps.createCartResponseBody.id,
      ctHeaders.headerUtil.getHeaders(),
    );
    await steps.whenIDelete();
    await steps.thenIShouldSeeCartDeletedSuccessfully();
  });
});

Environment variables

According to your project’s requirements, you can define your environment variables in this way:

.env.ct

An example is shown below for environment variables.

SERVICE_NAME=your-service
PORT=8093

ENABLE_DATABASE_SERVICE=true
ENABLE_DATABASE_SERVICE_LOGGING=true
ENABLE_TEST_LOGS=true

Package.json Component Test Automation Test Run Script

In package.json file in your service, you can use the below command to run your component test automation codes.

"scripts": {
    "test": "jest",
    "test:watch": "jest --watchAll",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:ct": "DOTENV_CONFIG_PATH=./.env.ct jest --setupFiles=dotenv/config -i --no-cache --config ./test/config/ct/jest.config.ts",
  },

Then you need to run the below command on the terminal windows.

npm run test:ct

Thanks,
Onur Baskirt

2 thoughts on “Component Test Automation for NestJS Microservices”

  1. Hi there!
    Thank you for your in details explanation. But I cannot find the link for course code. How can I find the course code link?
    Thank you again!

    Reply

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.