0
votes

Trying to write test scripts for my nestjs application.

I have controller/service framework, that looks like this:

Controller:

export class MyController {
    constructor(
        protected _svc: MyService
    ) {}

    @Get()
    async getAll(): Promise<Array<Person>> {
        return await this._svc.findAll();
    }
}

Service:

@Injectable()
export class MyService extends DbService < Person > {
constructor(
    private _cache: CacheService
) {
    super(...);
}

async findAll() {
    return super.findAll().then(res => {
        res.map(s => {
            this._cache.setValue(`key${s.ref}`, s);
        });
        return res;
    });
}

Base class:

@Injectable()
export abstract class DbService<T> {

    constructor() {}


    async findAll(): Promise<Array<T>> {
        ...
    }
}

My controller is the entry point when calling an endpoint on the API. This calls the service, which extends the DbService, which is what communicates with my database. There are a lot of services which all extend this DbService. In this case, the MyService class overrides the DbService "findAll" method to do some cache manipulation.

My test script has this:

let myController: MyController;
let myService: MyService;

describe("MyController", async () => {
    let spy_findall, spy_cacheset;
    beforeAll(() => {

        this._cacheService = {
            // getValue, setValue, delete methods
        };

        myService = new MyService(this._cacheService);
        myController = new MyController(myService);

        spy_findall = jest.spyOn(myService, "findAll").mockImplementation(async () => {
            return [testPerson];
        });

        spy_cacheset = jest.spyOn(this._cacheService, "setValue");
    });

    beforeEach(async () => {
        jest.clearAllMocks();
    });

    describe("getAll", () => {
        it("should return an array of one person", async () => {
            await myController.getAll().then(r => {
                expect(r).toHaveLength(1);
                expect(spy_findall).toBeCalledTimes(1);
                expect(spy_cacheset).toBeCalledTimes(1);
                expect(r).toEqual([testPerson]);
            });
        });
    });
});

Now, obviously the mockImplementation of findAll mocks the "findAll" on MyService, so the test fails because spy_cacheset is never called.

What I would like to do is mock only the base method "findAll" from DbService, so that I maintain the extra functionality that exists in MyService.

Is there a way of doing this without just renaming the methods in MyService, which I would rather avoid doing?

Edited to add: Thanks to @Jonatan lenco for such a comprehensive reponse, which I have taken on board and implemented. I have one further question. CacheService, DbService and a whole lot of other stuff (some of which I want to mock, other that I don't) is in an external library project, "shared".

cache.service.ts

export class CacheService {...}

index.ts

export * from "./shared/cache.service"
export * from "./shared/db.service"
export * from "./shared/other.stuff"
....

This is then compiled and included as a package in node_modules.

In the project where I am writing the tests:

import { CacheService, DocumentService, OtherStuff } from "shared";

Can I still use jest.mock() for just the CacheService, without mocking the whole "shared" project?

1

1 Answers

7
votes

In this case since you want to spy on an abstract class (DbService), you can spy on the prototype method:

jest.spyOn(DbService.prototype, 'findAll').mockImplementation(async () => {
  return [testPerson];
});

Also here some recommendations for your unit tests with NestJS and Jest:

  1. Use jest.mock() in order to simplify your mocking (in this case for CacheService). See https://jestjs.io/docs/en/es6-class-mocks#automatic-mock.

  2. When you do jest.spyOn(), you can assert the method execution without the need of the spy object. Instead of:

spy_findall = jest.spyOn(myService, "findAll").mockImplementation(async () => {
  return [testPerson];
});

...

expect(spy_findall).toBeCalledTimes(1);

You can do:

jest.spyOn(DbService.prototype, 'findAll').mockImplementation(async () => {
  return [testPerson];
});

...

expect(DbService.prototype.findAll).toBeCalledTimes(1);
  1. If you are mocking a class properly, you do not need to spy on the method (if you do not want to mock its implementation).

  2. Use the Testing utilities from NestJS, it will help you a lot specially when you have complex dependency injection. See https://docs.nestjs.com/fundamentals/testing#testing-utilities.

Here is an example that applies these 4 recommendations for your unit test:

import { Test } from '@nestjs/testing';

import { CacheService } from './cache.service';
import { DbService } from './db.service';
import { MyController } from './my.controller';
import { MyService } from './my.service';
import { Person } from './person';

jest.mock('./cache.service');

describe('MyController', async () => {
  let myController: MyController;
  let myService: MyService;
  let cacheService: CacheService;
  const testPerson = new Person();

  beforeAll(async () => {
    const module = await Test.createTestingModule({
      controllers: [MyController],
      providers: [
        MyService,
        CacheService,
      ],
    }).compile();

    myService = module.get<MyService>(MyService);
    cacheService = module.get<CacheService>(CacheService);
    myController = module.get<MyController>(MyController);

    jest.spyOn(DbService.prototype, 'findAll').mockImplementation(async () => {
      return [testPerson];
    });
  });

  beforeEach(async () => {
    jest.clearAllMocks();
  });

  describe('getAll', () => {
    it('Should return an array of one person', async () => {
      const r = await myController.getAll();
      expect(r).toHaveLength(1);
      expect(DbService.prototype.findAll).toBeCalledTimes(1);
      expect(cacheService.setValue).toBeCalledTimes(1);
      expect(r).toEqual([testPerson]);
    });
  });
});

NOTE: for the testing utilities to work and also for your application to work well, you will need to add the @Controller decorator on the class MyController:

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

...

@Controller()
export class MyController {

...

}

About mocking specific items of another package (instead of mocking the whole package) you could do this:

  1. Create a class in your spec file (or you can create it in another file that you import, or even in your shared module) which has a different name but has the same public method names. Note that we use jest.fn() since we do not need to provide an implementation, and that already spies in the method (no need to later do jest.spyOn() unless you have to mock the implementation).
class CacheServiceMock {
  setValue = jest.fn();
}
  1. When setting up the providers of your testing module, tell it that you are "providing" the original class but actually providing the mocked one:
const module = await Test.createTestingModule({
  controllers: [MyController],
  providers: [
    MyService,
    { provide: CacheService, useClass: CacheServiceMock },
  ],
}).compile();

For more info about providers see https://angular.io/guide/dependency-injection-providers (Nest follows the same idea of Angular).