196

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.

risingTide
  • 1,754
  • 7
  • 31
  • 60
CrazySynthax
  • 13,662
  • 34
  • 99
  • 183

11 Answers11

481

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');
blade
  • 12,057
  • 7
  • 37
  • 38
  • 88
    It's a shame the official documentation doesn't say this. The official documentation is an incomprehensible mess of module factories, class mocks hand rolled using object literals, stuff stored in special directories, and special case processing based on property names. https://jestjs.io/docs/en/es6-class-mocks#calling-jestmockdocsenjest-objectjestmockmodulename-factory-options-with-the-module-factory-parameter – Neutrino Jan 24 '21 at 10:56
  • I've edited my answer above. @blade Using `mockImplementation` will always yield a passing test and if the class changes, a false positive. – sesamechicken May 03 '21 at 16:56
  • 3
    @sesamechicken Not sure I follow with the always passing test. You should never test a mocked method directly, that would make zero sense. A usual use case is to mock a method that provides data for a different method that you're actually testing. – blade May 05 '21 at 12:07
  • >You should never test a mocked method directly That's exactly what the example above demonstrates using `mockImplementation`. – sesamechicken May 06 '21 at 13:54
  • 1
    It does, sure, but it answers the OP's question. I think it would be off-topic to go into details of correct testing in this answer and would detract from the point. – blade May 07 '21 at 16:04
  • @sesamechicken The above could be said to illustrate what's going on when the method being mocked is called instead of actually testing it. Does that sound better? – Lee Meador Jul 21 '21 at 17:57
  • @Neutrino And I thought it was just me – Mathemats Nov 17 '21 at 03:11
52

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);
  });
});
sesamechicken
  • 1,928
  • 14
  • 19
  • 18
    I don't know the answer to this, so genuinely curious: would this leave the Person.prototype.sayMyName altered for any other tests running after this one? – Martin Oct 08 '18 at 18:52
  • 10
    @Martin Yes, it does. – Frondor Oct 15 '18 at 18:44
  • 17
    I don't think this is a good practice. It's not using Jest or any other framework to mock the method and you'll need extra effort to restore the method. – Bruno Brant Sep 04 '19 at 15:16
  • 7
    See https://stackoverflow.com/a/56565849/1248209 for answer on how to do this properly in Jest using spyOn. – Lockyy Oct 21 '19 at 14:06
17

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)

ninhjs.dev
  • 7,203
  • 1
  • 49
  • 35
16

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");
});
madhamster
  • 181
  • 1
  • 5
11

rather than mocking the class you could extend it like this:

class MockedPerson extends Person {
  sayMyName () {
    return 'Hello'
  }
}
// and then
let person = new MockedPerson();
Billy Reilly
  • 1,422
  • 11
  • 11
8

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!

Saif Asad
  • 779
  • 10
  • 8
1

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");
    });
});
jolancornevin
  • 999
  • 1
  • 9
  • 7
1

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

Caleb Waldner
  • 889
  • 8
  • 11
1

I found a way to reproduce the original spyOn behaviour with Typescript and ES6 modules since you get a jest-Error nowadays when you try to use it on a class instance method.

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

The advantage of spyOn is that the original method still runs in its original implementation.

In my case the class instance is a mobX store. But I see no reason why it shouldnt work for other class modules.

The way to do it is simply to save a copy of the original method, then creating a mock function with the saved copy as mockImplemtation and the saving this back into the class instance


const storeThatNeedsToBeSpiedOn = new TodoStore();

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

const storeToTest = new SomeOtherStore(storeThatNeedsToBeSpiedOn);

and in the test:

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

the beauty of this is, that the original implementation of the method still runs with all its side effects (if there are any). So you could finish off your test by saying;

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

Hope this helps someone out there who is as frustrated as i was ;)

0

This is how you can mock class methods using jest:

Define my class "Person"

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

This is how we can mock:

Import axios:

    import axios from "axios";

Mock axios:

    jest.mock('axios');
    const mockedAxios = axios as jest.Mocked<typeof axios>;

Mock your class methods:

   let person = new Person("Shubham", "Verma");

    const sayMyNameFn = jest.spyOn(person.prototype as any, 'sayMyName');
    sayMyNameFn.mockResolvedValue("Shubham Verma");

    const blaFn = jest.spyOn(person.prototype as any, 'bla');
    blaFn.mockResolvedValue("bla");

Now, when you call Person.sayMyName it will not call the actual function but it will return/resolve with the string "Shubham Verma"

Shubham Verma
  • 8,783
  • 6
  • 58
  • 79
0

a lot a good answer here, but one more important thing:

it is not working if your method is an arrow method like this...

    sayMyName = () => {
        console.log(this.first + " " + this.last);
    };

If you use arrow functions in your classes, they will not be part of the mock. The reason for that is that arrow functions are not present on the object's prototype, they are merely properties holding a reference to a function.

source

Chris
  • 1,122
  • 1
  • 12
  • 14