尝试为我的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类将重写DbService的“ findAll”方法以进行一些缓存操作。
我的测试脚本具有以下内容:
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]);
});
});
});
});
现在,很明显,findAll的mockImplementation在MyService上模拟了“ findAll”,因此该测试失败了,因为从未调用过spy_cacheset。
我想做的只是模拟 only 来自DbService的基本方法“ findAll”,以便我维护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";
我是否仍可以仅将CacheService用作jest.mock(),而不模拟整个“共享”项目?
答案 0 :(得分:2)
在这种情况下,由于您要监视抽象类(DbService),因此可以监视原型方法:
jest.spyOn(DbService.prototype, 'findAll').mockImplementation(async () => {
return [testPerson];
});
这里还有一些有关使用NestJS和Jest进行单元测试的建议:
使用jest.mock()来简化模拟(在本例中为CacheService)。参见https://jestjs.io/docs/en/es6-class-mocks#automatic-mock。
执行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的Testing实用程序,当您进行复杂的依赖项注入时,它将特别为您提供很多帮助。参见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]);
});
});
});
注意:为使测试实用程序正常运行,并使应用程序正常运行,您需要在MyController类上添加@Controller装饰器:
import { Controller, Get } from '@nestjs/common';
...
@Controller()
export class MyController {
...
}