Jest: How to mock one specific method of a class
JavascriptJestjsJavascript Problem Overview
Let's suppose I have the following class:
export default class Person {
constructor(first, last) {
this.first = first;
this.last = last;
}
sayMyName() {
console.log(this.first + " " + this.last);
}
bla() {
return "bla";
}
}
Suppose I want to create a mocked class where method 'sayMyName' will be mocked and method 'bla' will stay as is.
The test I wrote is:
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");
})
The first 'expect' statement passes, which means that 'sayMyName' was mocked successfully. But, the second 'expect' fails with the error:
> TypeError: person.bla is not a function
I understand that the mocked class erased all methods. I want to know how to mock a class such that only specific method(s) will be mocked.
Javascript Solutions
Solution 1 - Javascript
Using jest.spyOn()
is the proper Jest way of mocking a single method and leaving the rest be. Actually there are two slightly different approaches to this.
1. Modify the method only in a single object
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. Modify the class itself, so that all the instances are affected
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");
});
And for the sake of completeness, this is how you'd mock a static method:
jest.spyOn(Person, 'myStaticMethod').mockImplementation(() => 'blah');
Solution 2 - Javascript
Edit 05/03/2021
I see a number of people disagree with the below approach, and that's cool. I do have a slight disagreement with @blade's approach, though, in that it actually doesn't test the class because it's using mockImplementation
. If the class changes, the tests will still always pass giving false positives. So here's an example with 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";
}
}
and the test:
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)
})
});
Cheers!
I don't see how the mocked implementation actually solves anything for you. I think this makes a bit more sense
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);
});
});
Solution 3 - Javascript
Have been asking similar question and I think figured out a solution. This should work no matter where Person class instance is actually used.
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");
});
Solution 4 - Javascript
Not really answer the question, but I want to show a use case where you want to mock a dependent class to verify another class.
For example: Foo
depends on Bar
. Internally Foo
created an instance of Bar
. You want to mock Bar
for testing Foo
.
Bar class
class Bar {
public runBar(): string {
return 'Real bar';
}
}
export default Bar;
Foo class
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;
The test:
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');
});
});
Note: this will not work if you use arrow functions to define methods in your class (as they are difference between instances). Converting it to regular instance method would make it work.
See also jest.requireActual(moduleName)
Solution 5 - Javascript
rather than mocking the class you could extend it like this:
class MockedPerson extends Person {
sayMyName () {
return 'Hello'
}
}
// and then
let person = new MockedPerson();
Solution 6 - Javascript
If you are using Typescript, you can do the following:
Person.prototype.sayMyName = jest.fn().mockImplementationOnce(async () =>
await 'my name is dev'
);
And in your test, you can do something like this:
const person = new Person();
const res = await person.sayMyName();
expect(res).toEqual('my name is dev');
Hope this helps someone!
Solution 7 - Javascript
I've combined both @sesamechicken and @Billy Reilly answers to create a util function that mock (one or more) specific methods of a class, without definitely impacting the class itself.
/**
* @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");
});
});
Solution 8 - Javascript
I was trying to get this to work on a class that had already been mocked. Because it had been mocked already, there was no prototype available for me to modify, so I found this workaround.
I don't love this solution, so if anyone knows a better way to update a method to a class that has already been mocked out, I'm all ears.
And just to clarify, the main answers to this question are working with classes that are not mocked out. In my situation, the class has already been mocked out and I'm trying to update one of the methods to the already-mocked class.
My solution:
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"
);
}),
};
});