Как сделать jest.spyOn только методом базового класса, а не переопределенным методом - PullRequest
0 голосов
/ 24 мая 2019

Попытка написать тестовые сценарии для моего приложения nestjs.

У меня есть контроллер / сервисная структура, которая выглядит следующим образом:

Контроллер:

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

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

Услуги:

@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;
    });
}

Базовый класс:

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

    constructor() {}


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

Мой контроллер является точкой входа при вызове конечной точки в API. Это вызывает сервис, который расширяет DbService, который связывается с моей базой данных. Есть много сервисов, которые расширяют этот DbService. В этом случае класс MyService переопределяет метод «findAll» DbService, чтобы выполнить некоторые манипуляции с кэшем.

Мой тестовый скрипт имеет следующее:

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]);
            });
        });
    });
});

Теперь, очевидно, mockImplementation findAll высмеивает «findAll» на MyService, поэтому тест не пройден, потому что spy_cacheset никогда не вызывается.

Я хотел бы только макет только базовый метод "findAll" из DbService, чтобы я поддерживал дополнительную функциональность, существующую в MyService.

Есть ли способ сделать это, не переименовывая методы в MyService, чего я бы предпочел избегать?

Отредактировано, чтобы добавить: Спасибо @Jonatan lenco за такой исчерпывающий ответ, который я принял и реализовал. У меня есть еще один вопрос. CacheService, DbService и множество других вещей (некоторые из которых я хочу издеваться, другие - нет) находятся во внешнем библиотечном проекте, «разделяемом».

cache.service.ts

export class CacheService {...}

index.ts

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

Затем он компилируется и включается как пакет в node_modules.

В проекте, в котором я пишу тесты:

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

Могу ли я по-прежнему использовать jest.mock () только для CacheService, без насмешек над всем "общим" проектом?

1 Ответ

2 голосов
/ 24 мая 2019

В этом случае, поскольку вы хотите шпионить за абстрактным классом (DbService), вы можете следить за методом-прототипом:

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

Также здесь приведены некоторые рекомендации для ваших модульных тестов с NestJS и Jest:

  1. Используйте jest.mock (), чтобы упростить вашу насмешку (в данном случае для CacheService).См. https://jestjs.io/docs/en/es6-class-mocks#automatic-mock.

  2. Когда вы делаете jest.spyOn (), вы можете утверждать выполнение метода без необходимости в шпионском объекте.Вместо:

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

...

expect(spy_findall).toBeCalledTimes(1);

Вы можете сделать:

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

...

expect(DbService.prototype.findAll).toBeCalledTimes(1);

Если вы правильно издеваетесь над классом, вам не нужно шпионить за методом (если вы не хотите издеваться над его реализацией).

Использовать тестированиеутилиты от NestJS, они вам особенно помогут, когда у вас сложное внедрение зависимостей.См. https://docs.nestjs.com/fundamentals/testing#testing-utilities.

Вот пример, который применяет эти 4 рекомендации для вашего модульного теста:

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]);
    });
  });
});

ПРИМЕЧАНИЕ: для работы утилит тестирования, а также для вашего приложениядля правильной работы вам потребуется добавить декоратор @Controller к классу MyController:

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

...

@Controller()
export class MyController {

...

}

. О том, что вы хотите смоделировать определенные элементы другого пакета (вместо того, чтобы насмехаться над всем пакетом), вы можете сделать это:

  1. Создайте класс в вашем spec-файле (или вы можете создать его в другом файле, который вы импортируете, или даже в вашем совместно используемом модуле), который имеет другое имя, но имеет те же имена открытых методов.Обратите внимание, что мы используем jest.fn (), так как нам не нужно предоставлять реализацию, и это уже шпионит в методе (нет необходимости позже делать jest.spyOn (), если вам не нужно высмеивать реализацию).
class CacheServiceMock {
  setValue = jest.fn();
}
При настройке поставщиков вашего модуля тестирования, скажите ему, что вы «предоставляете» исходный класс, но фактически предоставляете имитированный класс:
const module = await Test.createTestingModule({
  controllers: [MyController],
  providers: [
    MyService,
    { provide: CacheService, useClass: CacheServiceMock },
  ],
}).compile();

Для получения дополнительной информации о поставщиках см. https://angular.io/guide/dependency-injection-providers (Гнездо следует той же идее Angular).

...