【问题标题】:How to jest.spyOn only the base class method, not the overridden method如何 jest.spyOn 只有基类方法,而不是被覆盖的方法
【发布时间】:2019-10-10 23:24:24
【问题描述】:

尝试为我的 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 从未被调用过。

我想做的是模拟 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";

我是否仍然可以将 jest.mock() 仅用于 CacheService,而不模拟整个“共享”项目?

【问题讨论】:

    标签: node.js jestjs nestjs


    【解决方案1】:

    在这种情况下,由于您要监视抽象类(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 对象。而不是:

    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);
    
    1. 如果您正确地模拟了一个类,则不需要监视该方法(如果您不想模拟其实现)。

    2. 使用 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]);
        });
      });
    });
    

    注意:为了使测试实用程序正常工作以及您的应用程序正常工作,您需要在 MyController 类上添加 @Controller 装饰器:

    import { Controller, Get } from '@nestjs/common';
    
    ...
    
    @Controller()
    export class MyController {
    
    ...
    
    }
    

    关于模拟另一个包的特定项目(而不是模拟整个包),您可以这样做:

    1. 在您的规范文件中创建一个类(或者您可以在您导入的另一个文件中,甚至在您的共享模块中创建它),它具有不同的名称但具有相同的公共方法名称。请注意,我们使用 jest.fn() 是因为我们不需要提供实现,并且它已经在方法中进行了监视(以后无需执行 jest.spyOn(),除非您必须模拟实现)。
    class CacheServiceMock {
      setValue = jest.fn();
    }
    
    1. 在设置测试模块的提供者时,告诉它您正在“提供”原始类,但实际上提供的是模拟类:
    const module = await Test.createTestingModule({
      controllers: [MyController],
      providers: [
        MyService,
        { provide: CacheService, useClass: CacheServiceMock },
      ],
    }).compile();
    

    有关提供程序的更多信息,请参阅https://angular.io/guide/dependency-injection-providers(Nest 遵循与 Angular 相同的理念)。

    【讨论】:

    • 非常感谢您提供如此详细的回复。如果您有几分钟的时间,我希望您能快速查看我编辑中的问题。再次感谢!
    • 感谢您的好心cmets。我已经编辑了回复,将答案添加到您的最后一个问题。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-24
    • 2011-12-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多