Jest: 如何模仿类的一个特定方法

假设我有以下课程:

export default class Person {
constructor(first, last) {
this.first = first;
this.last = last;
}
sayMyName() {
console.log(this.first + " " + this.last);
}
bla() {
return "bla";
}
}

假设我想创建一个模拟类,其中方法‘ say MyName’将被模拟,而方法‘ bla’将保持不变。

我写的测试是:

const Person = require("../Person");


jest.mock('../Person', () => {
return jest.fn().mockImplementation(() => {
return {sayMyName: () => {
return 'Hello'
}};
});
});




let person = new Person();
test('MyTest', () => {
expect(person.sayMyName()).toBe("Hello");
expect(person.bla()).toBe("bla");
})

第一个“ Expect”语句通过,这意味着成功地模拟了“ say MyName”。但是,第二个“ Expect”失败,出现错误:

TypeError: Person.bla 不是一个函数

我知道模拟类擦除了所有方法。 我想知道如何模拟类,以便只模拟特定的方法。

172714 次浏览

而不是嘲笑班级,你可以这样扩展它:

class MockedPerson extends Person {
sayMyName () {
return 'Hello'
}
}
// and then
let person = new MockedPerson();

编辑05/03/2021

我看到很多人不同意下面的方法,这很酷。但是,我对@Blade 的方法有一点不同意,因为它实际上并不测试类,因为它使用的是 mockImplementation。如果类改变了,测试仍然会通过给出假阳性。这是 spyOn的一个例子。

// person.js
export default class Person {
constructor(first, last) {
this.first = first;
this.last = last;
}
sayMyName() {
return this.first + " " + this.last; // Adjusted to return a value
}
bla() {
return "bla";
}
}


还有测试:

import Person from './'


describe('Person class', () => {
const person = new Person('Guy', 'Smiley')


// Spying on the actual methods of the Person class
jest.spyOn(person, 'sayMyName')
jest.spyOn(person, 'bla')
  

it('should return out the first and last name', () => {
expect(person.sayMyName()).toEqual('Guy Smiley') // deterministic
expect(person.sayMyName).toHaveBeenCalledTimes(1)
});
it('should return bla when blah is called', () => {
expect(person.bla()).toEqual('bla')
expect(person.bla).toHaveBeenCalledTimes(1)
})
});

干杯!


我看不出模拟实现实际上是如何为您解决任何问题的

import Person from "./Person";


describe("Person", () => {
it("should...", () => {
const sayMyName = Person.prototype.sayMyName = jest.fn();
const person = new Person('guy', 'smiley');
const expected = {
first: 'guy',
last: 'smiley'
}


person.sayMyName();


expect(sayMyName).toHaveBeenCalledTimes(1);
expect(person).toEqual(expected);
});
});

如果您使用的是 Typecript,您可以执行以下操作:

Person.prototype.sayMyName = jest.fn().mockImplementationOnce(async () =>
await 'my name is dev'
);

在你的测试中,你可以这样做:

const person = new Person();
const res = await person.sayMyName();
expect(res).toEqual('my name is dev');

希望这对谁有帮助!

我已经将@esesamechicken 和@Billy Reilly 的回答结合在一起,创建了一个 util 函数,它模拟(一个或多个)类的特定方法,而不会明确地影响类本身。

/**
* @CrazySynthax class, a tiny bit updated to be able to easily test the mock.
*/
class Person {
constructor(first, last) {
this.first = first;
this.last = last;
}


sayMyName() {
return this.first + " " + this.last + this.yourGodDamnRight();
}


yourGodDamnRight() {
return ", you're god damn right";
}
}


/**
* Return a new class, with some specific methods mocked.
*
* We have to create a new class in order to avoid altering the prototype of the class itself, which would
* most likely impact other tests.
*
* @param Klass: The class to mock
* @param functionNames: A string or a list of functions names to mock.
* @returns {Class} a new class.
*/
export function mockSpecificMethods(Klass, functionNames) {
if (!Array.isArray(functionNames))
functionNames = [functionNames];


class MockedKlass extends Klass {
}


const functionNamesLenght = functionNames.length;
for (let index = 0; index < functionNamesLenght; ++index) {
let name = functionNames[index];
MockedKlass.prototype[name] = jest.fn();
};


return MockedKlass;
}


/**
* Making sure it works
*/
describe('Specific Mocked function', () => {
it('mocking sayMyName', () => {
const walter = new (mockSpecificMethods(Person, 'yourGodDamnRight'))('walter', 'white');


walter.yourGodDamnRight.mockReturnValue(", that's correct"); // yourGodDamnRight is now a classic jest mock;


expect(walter.sayMyName()).toBe("walter white, that's correct");
expect(walter.yourGodDamnRight.mock.calls.length).toBe(1);


// assert that Person is not impacted.
const saul = new Person('saul', 'goodman');
expect(saul.sayMyName()).toBe("saul goodman, you're god damn right");
});
});

一直在问类似的问题,我想找到了一个解决办法。无论在何处实际使用 Person 类实例,都应该这样做。

const Person = require("../Person");


jest.mock("../Person", function () {
const { default: mockRealPerson } = jest.requireActual('../Person');


mockRealPerson.prototype.sayMyName = function () {
return "Hello";
}


return mockRealPerson
});


test('MyTest', () => {
const person = new Person();
expect(person.sayMyName()).toBe("Hello");
expect(person.bla()).toBe("bla");
});

使用 jest.spyOn()是适当的 开玩笑的方法,它可以模拟单个方法,而其余的方法则保持不变。实际上有两种不同的方法。

1. 只修改单个对象中的方法

import Person from "./Person";


test('Modify only instance', () => {
let person = new Person('Lorem', 'Ipsum');
let spy = jest.spyOn(person, 'sayMyName').mockImplementation(() => 'Hello');


expect(person.sayMyName()).toBe("Hello");
expect(person.bla()).toBe("bla");


// unnecessary in this case, putting it here just to illustrate how to "unmock" a method
spy.mockRestore();
});

2. 修改类本身,使所有实例都受到影响

import Person from "./Person";


beforeAll(() => {
jest.spyOn(Person.prototype, 'sayMyName').mockImplementation(() => 'Hello');
});


afterAll(() => {
jest.restoreAllMocks();
});


test('Modify class', () => {
let person = new Person('Lorem', 'Ipsum');
expect(person.sayMyName()).toBe("Hello");
expect(person.bla()).toBe("bla");
});

为了完整起见,下面是模仿静态方法的方法:

jest.spyOn(Person, 'myStaticMethod').mockImplementation(() => 'blah');

我并没有真正回答这个问题,但是我想展示一个用例,在这个用例中,您想要模拟一个依赖类来验证另一个类。

例如: Foo依赖于 Bar.ABC0在内部创建了 Bar的一个实例。你想要模拟 Bar来测试 Foo

酒吧课程

class Bar {
public runBar(): string {
return 'Real bar';
}
}


export default Bar;

Foo 课

import Bar from './Bar';


class Foo {
private bar: Bar;


constructor() {
this.bar = new Bar();
}


public runFoo(): string {
return 'real foo : ' + this.bar.runBar();
}
}


export default Foo;




测试:

import Foo from './Foo';
import Bar from './Bar';


jest.mock('./Bar');


describe('Foo', () => {
it('should return correct foo', () => {
// As Bar is already mocked,
// we just need to cast it to jest.Mock (for TypeScript) and mock whatever you want
(Bar.prototype.runBar as jest.Mock).mockReturnValue('Mocked bar');
const foo = new Foo();
expect(foo.runFoo()).toBe('real foo : Mocked bar');
});
});




注意: 如果使用箭头函数来定义类中的方法(因为它们在实例之间是不同的) ,这将不起作用。将其转换为常规实例方法可以使其正常工作。

参见 要求实际(模块名)

我试图让这个在一个已经被嘲笑过的类中起作用。因为它已经被模拟过了,所以没有原型可供我修改,所以我找到了这个变通方法。

我不喜欢这个解决方案,所以如果有人知道更好的方法来更新一个已经被模拟过的类的方法,我洗耳恭听。

只是为了澄清一下,这个问题的主要答案是使用不被嘲弄的类。在我的情况下,类已经被模拟出来了,我正试图将其中一个方法更新到已经被模拟的类中。

我的解决办法是:


const previousClassInstance = new PreviouslyMockedClass();
PreviouslyMockedClass.mockImplementation(() => {
return {
// "Import" the previous class methods at the top
...previousClassInstance,


// Then overwrite the ones you wanna update
myUpdatedMethod: jest.fn(() => {
console.log(
"This method is updated, the others are present and unaltered"
);
}),
};
});


我找到了一种使用 Typecript 和 ES6模块重现原始 spyOn行为的方法,因为现在当您试图在类实例方法上使用它时,您会得到一个 jest-Error。

const addTodoSpy = jest.spyOn(storeThatNeedsToBeSpiedOn, 'addTodo');
TypeError: Cannot redefine property: addTodo at Function.defineProperty (<anonymous>)

SpyOn 的优点是原始方法仍然在其原始实现中运行。

在我的例子中,类实例是一个 mobX 存储。但是我看不出为什么它不能用于其他类模块。

这样做的方法很简单,就是保存原始方法的一个副本,然后创建一个模拟函数,将保存的副本作为模拟实现,并将其保存回类实例中


const storeThatNeedsToBeSpiedOn = new TodoStore();


const keep = storeThatNeedsToBeSpiedOn.addTodo;
const addTodoSpy = jest.fn().mockImplementation(keep);
storeThatNeedsToBeSpiedOn.addTodo = addTodoSpy;


const storeToTest = new SomeOtherStore(storeThatNeedsToBeSpiedOn);

在测试中:

storeToTest.methodThatCallsAddTodoInternally();
expect(addTodoSpy).toBeCalledTimes(1);

这样做的好处在于,方法的原始实现仍然带着它的所有副作用(如果有的话)运行。所以你可以说:

expect(storeThatNeedsToBeSpiedOn.todos.length).toEqual(/* one more than before */);

希望这能帮助那些和我一样沮丧的人;)