如何模拟 class 的特定方法,同时在 class 实例不可访问时保持所有其他方法的实现?

How to mock a specific method of a class whilst keeping the implementation of all other methods with jest when the class instance isn't accessible?

基于这个问题 (),如何在保持所有其他方法的实现的同时模拟特定方法?

有一个类似的问题 () but this only applies if the class instance is available outside it's calling class so this wouldn't work if the class instance was inside a constructor like in this question ()。

例如,Logger class 被模拟为只有 method1 被模拟,但 method2 丢失,导致错误:

// Logger.ts
export default Logger() {
    constructor() {}
    method1() {
        return 'method1';
    }
    method2() {
        return 'method2';
    }
}

// Logger.test.ts
import Logger from './Logger';

jest.mock("./Logger", () => {
    return {
        default: class mockLogger {
            method1() {
                return 'mocked';
            }
        },
        __esModule: true,
    };
});

describe("Logger", () => {
    it("calls logger.method1() & logger.method2 on instantiation where only method1 is mocked", () => {
        const logger = new Logger(); // Assume this is called in the constructor of another object.

        expect(logger.method1()).toBe('mocked');
        expect(logger.method2()).toBe('method2'); // TypeError: logger.method2 is not a function.
    });
});

一个解决方案是扩展 Logger class 但这会导致 undefined 错误,因为 Logger 已经被模拟:

// ...
jest.mock("./Logger", () => {
    return {
        default: class mockLogger extends Logger {
            override method1() {
                return 'mocked';
            }
        },
        __esModule: true,
    };
});
// ...
expect(logger.method2()).toBe('method2'); // TypeError: Cannot read property 'default' of undefined

因此,仅模拟 method1 但保留 method2 的原始实现的正确方法是什么?

模拟原型有效:

describe("Logger", () => {
    it("calls logger.method1() & logger.method2 on instantiation where only method1 is mocked", () => {
        Logger.prototype.method1 = jest.fn(() => 'mocked');
        const logger = new Logger();

        expect(logger.method1()).toBe('mocked');
        expect(logger.method2()).toBe('method2');
    });
});

但是,当 class 实例不可访问时,我不确定这是否是模拟特定方法的正确方法,所以我会暂时保留这个问题,以防有更好的方法解决方案。

您可以使用 jest.spyOn 并为 method1 提供模拟实现。

// Logger.test.ts
import Logger from './Logger';

jest.spyOn(Logger.prototype, "method1").mockImplementation(() => "mocked")

describe("Logger", () => {
  it("calls method1 & method2 but only method1 is mocked", () => {
    const l = new Logger();
    expect(l.method1()).toBe("mocked");
    expect(l.method2()).toBe("method2");
  })
})

但是如果你有很多方法并且你想模拟除了一个方法之外的每个方法,那么你可以使用 jest.requireActual.

获得这个单一方法的原始实现。
// Logger.test.ts
import Logger from "./Logger";

const mockMethod1 = jest.fn().mockReturnValue("mocked");
const mockMethod3 = jest.fn().mockReturnValue("mocked");
const mockMethod4 = jest.fn().mockReturnValue("mocked");
const mockMethod5 = jest.fn().mockReturnValue("mocked");
jest.mock("./Logger", () =>
  jest.fn().mockImplementation(() => ({
    method1: mockMethod1,
    method2: jest.requireActual("./Logger").default.prototype.method2,
    method3: mockMethod3,
    method4: mockMethod4,
    method5: mockMethod5,
  }))
);

describe("Logger", () => {
  it("calls all methods but only method1 is mocked", () => {
    const l = new Logger();
    expect(l.method1()).toBe("mocked");
    expect(l.method2()).toBe("method2");
    expect(l.method3()).toBe("mocked");
    expect(l.method4()).toBe("mocked");
    expect(l.method5()).toBe("mocked");
  });
});

注意: 你不需要定义一个 ES6 class 来模拟,一个构造函数也可以正常工作,因为 ES6 classes 是实际上只是构造函数的语法糖。