Chapter 14. Testing

Automated testing is a critical part of software development. Even though it cannot (and it’s not intended) to replace manual testing and other quality assurance methods. Automated testing is a very valuable tool when used properly, to avoid regressions, bugs, or incorrect functionality.

Software development is a tricky discipline: even though many developers try to isolate different parts of software, it’s often unavoidable that some pieces of the puzzle have effect on other pieces, be it intended or unintended.

One of the main goals of using automated testing is to detect the kind of errors in which new code might break previously working functionality. These tests are known as regression tests, and they make the most sense when triggered as part of the merging or deployment process, as a required step. This means that, if the automated tests fail, the merge or deployment will be interrupted, thus avoiding the introduction of new bugs to the main codebase or to productive environments.

Automated testing also enables a developmental workflow known as test driven development (TDD.) When following a TDD approach, automated tests are written beforehand, as very specific cases that reflect the requirements. Once the new tests are written, the developer runs all tests; the new ones should fail, since no new code has been written yet. At that point it’s when the new code has to be written so that the new tests pass, while not breaking the old ones.

The test driven development approach, if done right, can improve the confidence in code quality and requirement compliance. They can also make refactoring or even full code migrations, less risky.

There are two main types of automated tests we are going to cover in this book: unit tests and end-to-end tests.

Unit testing

As the name implies, each unit test cover one specific functionality. The most important principles when dealing with unit tests are:

  • isolation; each component has to be tested without any other related components; it cannot be affected by side effects and, likewise, it cannot emit any side effects.
  • predictability; each test has to yield the same results as long as the input doesn’t change.

In many cases, complying with these two principles means mocking (i.e. simulating the functionality of) the component dependencies.

Tooling

Unlike Angular, Nest.js doesn’t have an “official” toolset for running tests; this means we are free to set up our own tooling for running automated tests when we work in Nest.js projects.

There are multiple tools in the JavaScript ecosystem focused on writing and running automated unit tests. The typical solutions involve using several different packages for one setup, because those packages used to be limited in their scope (one for test running, a second one for assertions, a third one for mocking, maybe even another one for code coverage reporting).

We will, however, be using Jest, an “all-in-one”, “zero-configuration” testing solution from Facebook that has greatly decreased the amount of configuration effort needed for running automated tests. It also officially supports TypeScript so it’s a great match for Nest.js projects!

Preparation

As you would expect, Jest is distributed as an npm package. Let’s go and install it in our project. Run the following command from your command line or terminal:

npm install --save-dev jest ts-jest @types/jest

We are installing three different npm packages as development dependencies: Jest itself; ts-jest that allows us to use Jest with our TypeScript code; and the Jest typings for their valuable contribution to our IDE experience!

Remember how we mentioned that Jest is a “zero-configuration” testing solution? Well, this is what their homepage claims. Unfortunately, it’s not entirely true: we still need to define a little amount of configuration before we are able to run our tests. In our case, this is mostly because we are using TypeScript. On the other hand, the configuration we need to write is really not that much, so we can write it as a plain JSON object.

So, let’s create a new JSON file in our project’s root folder, which we will name nest.json.

/nest.json

{
  "moduleFileExtensions": ["js", "ts", "json"],
  "transform": {
    "^.+\\.ts": "<rootDir>/node_modules/ts-jest/preprocessor.js"
  },
  "testRegex": "/src/.*\\.(test|spec).ts",
  "collectCoverageFrom": [
    "src/**/*.ts",
    "!**/node_modules/**",
    "!**/vendor/**"
  ],
  "coverageReporters": ["json", "lcov", "text"]
}

This small JSON file is setting up the following configuration:

  1. Established files with .js, .ts and .json as modules (i.e. code) of our app. You might think we would not need .js files, but the truth is that our code will not run without that extension, because of some of Jest’s own dependencies.
  2. Tells Jest to process files with a .ts extension using the ts-jest package (which was installed before from the command line).
  3. Specifies that our test files will be inside the /src folder, and will have either the .test.ts or the .spec.ts files extension.
  4. Instructs Jest to generate code coverage reports from any .ts file on the /src folder, while ignoring the node_modules and the vendor folder contents. Also, to generate coverage it reports in both JSON and LCOV formats.

Finally, the last step before we can start writing tests will be to add a couple of new scripts to your package.json file:

{
  ...
  "scripts": {
    ...
    "test": "jest --config=jest.json",
    "test:watch": "jest --watch --config=jest.json",
    ...
  }
}

The three new scripts will, respectively: run the tests once, run the tests in watch mode (they will run after each file save), and run the tests and generate the code coverage report (which will be output in a coverage folder).

NOTE: Jest receives its configuration as a jest property in the package.json file. If you decide to do things that way, you will need to omit the --config=jest.json arguments on your npm scripts.

Our testing environment is ready. If we now run npm test in our project folder, we will most likely see the following:

No tests found
In /nest-book-example
  54 files checked.
  testMatch:  - 54 matches
  testPathIgnorePatterns: /node_modules/ - 54 matches
  testRegex: /src/.*\.(test|spec).ts - 0 matches
Pattern:  - 0 matches
npm ERR! Test failed.  See above for more details.

The tests have failed! Well, they actually haven’t; we just have not written any tests yet! Let’s write some now.

Writing our first test

If you have read some more chapters of the book, you probably remember our blog entries and the code we wrote for them. Let’s take a look back to the EntryController. Depending on the chapters, the code looked something like the following:

/src/modules/entry/entry.controller.ts

import { Controller, Get, Post, Param } from '@nestjs/common';

import { EntriesService } from './entry.service';

@Controller('entries')
export class EntriesController {
  constructor(private readonly entriesSrv: EntriesService) {}

  @Get()
  findAll() {
    return this.entriesSrv.findAll();
  }
  ...
}

Notice that this controller is a dependency to EntriesService. Since we mentioned that each component has to be tested in isolation, we will need to mock any dependency it might have; in this case, the EntriesService.

Let’s write a unit test for the controller’s findAll() method. We will be using a special Nest.js package called @nestjs/testing, which will alow us to wrap our service in a Nest.js module specially for the test.

Also, it’s important to follow the convention and name the test file entry.controller.spec.ts, and place it next to the entry.controller.ts file, so it gets properly detected by Jest when we trigger a test run.

/src/modules/entry/entry.controller.spec.ts

import { Test } from '@nestjs/testing';
import { EntriesController } from './entry.controller';
import { EntriesService } from './entry.service';

describe('EntriesController', () => {
  let entriesController: EntriesController;
  let entriesSrv: EntriesService;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      controllers: [EntriesController],
    })
      .overrideComponent(EntriesService)
      .useValue({ findAll: () => null })
      .compile();

    entriesSrv = module.get<EntriesService>(EntriesService);
    entriesController = module.get<EntriesController>(EntriesController);
  });
});

Let’s now take a close look at what the test code is achieving.

First of all, we are declaring a test suite on describe('EntriesController', () => {. We also declare a couple of variables, entriesController and entriesSrv, to hold both the tested controller itself, as well as the service the controller depends on.

Then, it comes the beforeEach method. The code inside that method will be executed right before each of the following tests are run. In that code, we are instantiating a Nest.js module for each test. Note that this is a particular kind of module, since we are using the .createTestingModule() method from the Test class that comes from the @nestjs/testing package. So, let’s think about this module as a “mock module,” which will serve us for testing purposes only.

Now comes the fun part: we include the EntriesController as a controller in the testing module. We then proceed to use:

.overrideComponent(EntriesService)
.useValue({ findAll: () => null })

This substitutes the original EntryService, which is a dependency of our tested controller. This is for a mock version of the service, which is not even a class, since we don’t need it to be, but rather an object with a findAll method that takes no arguments and returns null.

You can think of the result of the two code lines above as an empty, dumb service that only repeats the methods we will need to use later, without any implementation inside.

Finally, the .compile() method is the one that actually instantiates the module, so it gets bound to the module constant.

Once the module is properly instantiated, we can bind our previous entriesController and entriesSrv variables to the instances of the controller and the service inside the module. This is achieved with the module.get method call.

Once all this initial setup is done, we are good to start writing some actual tests. Let’s implement one that checks whether the findAll() method in our controller correctly returns an array of entries, even if we only have one entry:

import { Test } from '@nestjs/testing';
import { EntriesController } from './entry.controller';
import { EntriesService } from './entry.service';

describe('EntriesController', () => {
  let entriesController: EntriesController;
  let entriesSrv: EntriesService;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      controllers: [EntriesController],
    })
      .overrideComponent(EntriesService)
      .useValue({ findAll: () => null })
      .compile();

    entriesSrv = module.get<EntriesService>(EntriesService);
    entriesController = module.get<EntriesController>(EntriesController);
  });

  describe('findAll', () => {
    it('should return an array of entries', async () => {
      expect(Array.isArray(await entriesController.findAll())).toBe(true);
    });
  });
});

The describe('findAll', () => { line is the one that starts the actual test suite. We expect the resolved value of entriesController.findAll() to be an array. This is basically how we wrote the code in the first place, so it should work, right? Let’s run the tests with npm test and check the test output.

FAIL  src/modules/entry/entry.controller.spec.ts
  EntriesController
    findAll
      ✕ should return an array of entries (4ms)

  ● EntriesController › findAll › should return an array of entries

    expect(received).toBe(expected) // Object.is equality

    Expected value to be:
      true
    Received:
      false

      30 |       ];
      31 |       // jest.spyOn(entriesSrv, 'findAll').mockImplementation(() => result);
    > 32 |       expect(Array.isArray(await entriesController.findAll())).toBe(true);
      33 |     });
      34 |
      35 |     // it('should return the entries retrieved from the service', async () => {

      at src/modules/entry/entry.controller.spec.ts:32:64
      at fulfilled (src/modules/entry/entry.controller.spec.ts:3:50)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        1.112s, estimated 2s
Ran all test suites related to changed files.

It failed... Well, of course it failed! Remember the beforeEach() method?

...
.overrideComponent(EntriesService)
.useValue({ findAll: () => null })
.compile();
...

We told Nest.js to exchange the original findAll() method in the service for another one that returns just null. We will need to tell Jest to mock that method with something that returns an array, so to check that when the EntriesService returns an array, the controller is in fact returning that result as an array as well.

...
describe('findAll', () => {
  it('should return an array of entries', async () => {
    jest.spyOn(entriesSrv, 'findAll').mockImplementationOnce(() => [{}]);
    expect(Array.isArray(await entriesController.findAll())).toBe(true);
  });
});
...

In order to mock the findAll() method from the service, we are using two Jest methods. spyOn() takes an object and a method as arguments, and starts watching the method for its execution (in other words, sets up a spy). And mockImplementationOnce(), which as its name implies changes the implementation of the method when it’s next called (in this case, we change it to return an array of one empty object.)

Let’s try to run the test again with npm test:

 PASS  src/modules/entry/entry.controller.spec.ts
  EntriesController
    findAll
      ✓ should return an array of entries (3ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.134s, estimated 2s
Ran all test suites related to changed files.

The test is passing now, so you can be sure that the findAll() method on the controller will always behave itself and return an array, so that other code components that depend on this output being an array won’t break themselves.

If this test started to fail at some point in the future, it would mean that we had introduced a regression in our codebase. One of the great sides of automated testing is that we will be notified about this regression before it’s too late.

Testing for equality

Up until this point, we are sure that EntriesController.findAll() returns an array. We can’t be sure that it’s not an array of empty objects, or an array of booleans, or just an empty array. In other words, we could rewrite the method to something like findAll() { return []; } and the test would still pass.

So, let’s improve our tests to check that the method really returns the output from the service, without messing things up.

import { Test } from '@nestjs/testing';
import { EntriesController } from './entry.controller';
import { EntriesService } from './entry.service';

describe('EntriesController', () => {
  let entriesController: EntriesController;
  let entriesSrv: EntriesService;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      controllers: [EntriesController],
    })
      .overrideComponent(EntriesService)
      .useValue({ findAll: () => null })
      .compile();

    entriesSrv = module.get<EntriesService>(EntriesService);
    entriesController = module.get<EntriesController>(EntriesController);
  });

  describe('findAll', () => {
    it('should return an array of entries', async () => {
      jest.spyOn(entriesSrv, 'findAll').mockImplementationOnce(() => [{}]);
      expect(Array.isArray(await entriesController.findAll())).toBe(true);
    });

    it('should return the entries retrieved from the service', async () => {
      const result = [
        {
          uuid: '1234567abcdefg',
          title: 'Test title',
          body:
            'This is the test body and will serve to check whether the controller is properly doing its job or not.',
        },
      ];
      jest.spyOn(entriesSrv, 'findAll').mockImplementationOnce(() => result);

      expect(await entriesController.findAll()).toEqual(result);
    });
  });
});

We just kept most of the test file as it was before, although we did add a new test, the last one, in which:

  • We set an array of one not-empty object (the result constant).
  • We mock the implementation of the service’s findAll() method once again to return that result.
  • We check that the controller returns the result object exactly as the original when called. Note that we are using the Jest’s .toEqual() method which, unlike .toBe(), performs a deep equality comparison between both objects for all of their properties.

This is what we get when we run npm test again:

 PASS  src/modules/entry/entry.controller.spec.ts
  EntriesController
    findAll
      ✓ should return an array of entries (2ms)
      ✓ should return the entries retrieved from the service (1ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        0.935s, estimated 2s
Ran all test suites related to changed files.

Both our tests pass. We accomplished quite a lot already. Now that we have a solid foundation, extending our tests to cover as many test cases as possible will be an easy task.

Of course, we have only written a test for a controller. But testing services and the rest of the pieces of our Nest.js app works the same way.

Covering our code in tests

One critical aspect in code automation is code coverage reporting. Because, how do you know that your tests actually cover as many test cases as possible? Well, the answer is checking code coverage.

If you want to be really confident in your tests as a regression detection systems, make sure that they cover as much functionality as possible. Let’s imagine we have a class with five methods and we only write tests for two of them. We would have roughly two-fifths of the code covered with tests, which means that we won’t have any insights about the other three-fifths, and about whether they still work as our codebase keeps on growing.

Code coverage engines analyze our code and tests together, and check the amount of lines, statements, and branches that are covered by the tests running in our suites, returning a percentage value.

As mentioned in previous sections, Jest already includes code coverage reporting out of the box, you just need to activate it by passing a --coverage argument to the jest command.

Let’s add a script in our package.json file that, when executed, will generate the coverage report:

{
  ...
  "scripts": {
    ...
    "test:coverage":"jest --config=jest.json --coverage --coverageDirectory=coverage",
    ...
  }
}

When running npm run test:coverage on the controller written before, you will see the following output:

 PASS  src/modules/entry/entry.controller.spec.ts
  EntriesController
    findAll
      ✓ should return an array of entries (9ms)
      ✓ should return the entries retrieved from the service (2ms)

---------------------|----------|----------|----------|----------|-------------------|
File                 |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
---------------------|----------|----------|----------|----------|-------------------|
All files            |      100 |    66.67 |      100 |      100 |                   |
 entry.controller.ts |      100 |    66.67 |      100 |      100 |                 6 |
---------------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        4.62s
Ran all test suites.

In order to have a better vision of the console output within this book, we will transform the console output to a proper table.

File % Stmts % Branch % Funcs % Lines Uncovered Line #s
All files 100 66.67 100 100
entry.controller.ts 100 66.67 100 100 6

You can easily see we are covering 100% of our code lines in our tests. This makes sense since we wrote two tests for the only method in our controller.

Failing tests for low coverage

Let’s imagine now that we work in a complex project with several developers working on the same base at the same time. Let’s imagine also that our workflow includes a Continuous Integration/Continuous Delivery pipeline, running on something like Travis CI, CircleCI, or even Jenkins. Our pipeline would likely include a step that runs our automated tests before merging or deploying, so that the pipeline will be interrupted if the tests fail.

All the imaginary developers working in this imaginary project will add (as well as refactor and delete, but those cases don’t really apply to this example) new functionality (i.e. new code) all the time, but they might forget about properly testing that code. What would happen then? The coverage percentage value of the project would go down.

In order to still be sure that we can rely on our tests as a regression detection mechanism, we need to be sure that the coverage never goes too low. What is too low? That really depends on multiple factors: the project and the stack it uses, the team, etc. However, it’ normally a good rule of thumb not letting the coverage value go down on each coding process iteration.

Anyway, Jest allows you to specify a coverage threshold for tests: if the value goes below that threshold, the tests will return failed even if they all passed. This way, our CI/CD pipeline will refuse to merge or deploy our code.

The coverage threshold has to be included in the Jest configuration object; in our case, it lives in the jest.json file in our project’s root folder.

{
  ...
  "coverageThreshold": {
    "global": {
      "branches": 80,
      "functions": 80,
      "lines": 80,
      "statements": 80
    }
  }
}

Each number passed to each property of the object is a percentage value; below it, the tests will fail.

To demonstrate it, let’s run our controller tests with the coverage threshold set as above. npm run test:coverage returns this:

 PASS  src/modules/entry/entry.controller.spec.ts
  EntriesController
    findAll
      ✓ should return an array of entries (9ms)
      ✓ should return the entries retrieved from the service (1ms)

---------------------|----------|----------|----------|----------|-------------------|
File                 |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
---------------------|----------|----------|----------|----------|-------------------|
All files            |      100 |    66.67 |      100 |      100 |                   |
 entry.controller.ts |      100 |    66.67 |      100 |      100 |                 6 |
---------------------|----------|----------|----------|----------|-------------------|
Jest: "global" coverage threshold for branches (80%) not met: 66.67%
Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.282s, estimated 4s
Ran all test suites.
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! nest-book-example@1.0.0 test:coverage: `jest --config=jest.json --coverage --coverageDirectory=coverage`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the nest-book-example@1.0.0 test:coverage script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

As you can see, the tests passed, yet the process failed with status 1 and returned an error. Also, Jest reported "global" coverage threshold for branches (80%) not met: 66.67%. We have successfully kept non-acceptable code coverage away from our main branch or productive environments.

The following step could be now to implement a few end-to-end tests, along with our unit tests, to improve our system.

E2E testing

While unit tests are isolated and independent by definition, end-to-end (or E2E) tests serve for, in a way, the opposite function: they intend to check the health of the system as a whole, and try to include as many components of the solution as possible. For this reason, in E2E tests we will focus on testing complete modules, rather than isolated components or controllers.

Preparation

Fortunately, we can use Jest for E2E testing just like we did for unit testing. We will only need to install the supertest npm package to perform API requests and assert their result. Let’s install it by running npm install --save-dev supertest in your console.

Also, we will create a folder called e2e in our project’s root folder. This folder will hold all of our E2E test files, as well as the configuration file for them.

This brings us to the next step: create a new jest-e2e.json file inside the e2e folder with the following contents:

{
  "moduleFileExtensions": ["js", "ts", "json"],
  "transform": {
    "^.+\\.tsx?$": "<rootDir>/node_modules/ts-jest/preprocessor.js"
  },
  "testRegex": "/e2e/.*\\.(e2e-test|e2e-spec).ts|tsx|js)$",
  "coverageReporters": ["json", "lcov", "text"]
}

As you can see, the new E2E configuration object is very similar to the one for unit tests; the main difference is the testRegex property, which now points to files in the /e2e/ folder that have a .e2e-test or e2e.spec file extension.

The final step of the preparation will be to include an npm script in our package.json file to run the end-to-end tests:

{
  ...
  "scripts": {
    ...
    "e2e": "jest --config=e2e/jest-e2e.json --forceExit"
  }
  ...
}

Writing end-to-end tests

The way of writing E2E tests with Jest and Nest.js is also very similar to the one we used for unit tests: we create a testing module using the @nestjs/testing package, we override the implementation for the EntriesService to avoid the need for a database, and then we are ready to run our tests.

Let’s write the code for the test. Create a new folder called entries inside the e2e folder, and then create a new file there called entries.e2e-spec.ts with the following content:

import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';

import { EntriesModule } from '../../src/modules/entry/entry.module';
import { EntriesService } from '../../src/modules/entry/entry.service';

describe('Entries', () => {
  let app: INestApplication;
  const mockEntriesService = { findAll: () => ['test'] };

  beforeAll(async () => {
    const module = await Test.createTestingModule({
      imports: [EntriesModule],
    })
      .overrideComponent(EntriesService)
      .useValue(mockEntriesService)
      .compile();

    app = module.createNestApplication();
    await app.init();
  });

  it(`/GET entries`, () => {
    return request(app.getHttpServer())
      .get('/entries')
      .expect(200)
      .expect({
        data: mockEntriesService.findAll(),
      });
  });

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

Let’s review what the code does:

  1. The beforeAll method creates a new testing module, imports the EntriesModule in it (the one we are going to test), and overrides the EntriesService implementation with the very simple mockEntriesService constant. Once that’s done, it uses the .createNestApplication() method to create an actual running app to make requests to, and then waits for it to be initialized.
  2. The '/GET entries' test uses a supertest to perform a GET request to the /entries endpoint, and then asserts whether the status code of the response from that request was a 200 and the received body of the response matches the mockEntriesService constant value. If the test passes, it will mean that our API is correctly reacting to requests received.
  3. The afterAll method ends the Nest.js app we created when all of the tests have run. This is important to avoid side effects when we run the tests the next time.

Summary

In this chapter we have explored the importance of adding automated tests to our projects and what kind of benefits it brings.

Also, we got started with the Jest testing framework, and we learned how to configure it in order to use it seamlessly with TypeScript and Nest.js

Lastly, we reviewed how to use the testing utilities that Nest.js provides for us, and learned how to write tests, both unit tests as well as end-to-end ones, and how to check the percentage of the code our tests are covering.

In the next and last chapter we cover server-side rendering with Angular Universal.