Jest: How to mock one specific method of a class

JavascriptJestjs

Javascript 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"
            );
        }),
    };
});

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionCrazySynthaxView Question on Stackoverflow
Solution 1 - JavascriptbladeView Answer on Stackoverflow
Solution 2 - JavascriptsesamechickenView Answer on Stackoverflow
Solution 3 - JavascriptmadhamsterView Answer on Stackoverflow
Solution 4 - JavascriptNinh PhamView Answer on Stackoverflow
Solution 5 - JavascriptBilly ReillyView Answer on Stackoverflow
Solution 6 - JavascriptSaif AsadView Answer on Stackoverflow
Solution 7 - JavascriptjolancornevinView Answer on Stackoverflow
Solution 8 - JavascriptCaleb WaldnerView Answer on Stackoverflow