5
votes

I'd like to augment, but not completely replace, instances of a mocked constructor in a Jest unit test.

I want to add a few values to the instance, but keep the auto-mocked goodness of Jest.

For example:

A.js

module.exports = class A {
  constructor(value) {
    this.value = value;
  }
  getValue() {
    return this.value;
  }
}

To get some auto-mock awesomeness:

jest.mock('./A');

With the automock, instances have a mocked .getValue() method, but they do not have the .value property.

A documented way of mocking constructors is:

// SomeClass.js
module.exports = class SomeClass {
  m(a, b) {}
}

// OtherModule.test.js
jest.mock('./SomeClass');  // this happens automatically with automocking
const SomeClass = require('./SomeClass')
const mMock = jest.fn()
SomeClass.mockImplementation(() => {
  return {
    m: mMock
  }
})

const some = new SomeClass()
some.m('a', 'b')
console.log('Calls to m: ', mMock.mock.calls)

Using that approach for A:

jest.mock('./A');

const A = require('./A');

A.mockImplementation((value) => {
  return { value };
});

it('does stuff', () => {
  const a = new A();
  console.log(a); // -> A { value: 'value; }
});

The nice thing about that is you can do whatever you want to the returned value, like initialize .value.

The downsides are:

  • You don't get any automocking for free, e.g. I'd need to add .getValue() myself to the instance
  • You need to have a different jest.fn() mock function for each instance created, e.g. if I create two instances of A, each instance needs its own jest.fn() mock functions for the .getValue() method
  • SomeClass.mock.instances is not populated with the returned value (GitHub ticket)

One thing that didn't work (I was hoping that maybe Jest did some magic):

A.mockImplementation((value) => {
  const rv = Object.create(A.prototype); // <- these are mocked methods
  rv.value = value;
  return rv;
});

Unfortunately, all instances share the same methods (as one would expect, but it was worth a shot).

My next step is to generate the mock, myself, via inspecting the prototype (I guess), but I wanted to see if there is an established approach.

Thanks in advance.

2

2 Answers

2
votes

Turns out this is fixed (as of jest 24.1.0) and the code in the question works, as expected.


To recap, given class A:

A.js

module.exports = class A {
  constructor(value) {
    this.value = value;
  }
  setValue(value) {
    this.value = value;
  }
}

This test will now pass:

A.test.js

jest.mock('./A');

const A = require('./A');

A.mockImplementation((value) => {
  const rv = Object.create(A.prototype); // <- these are mocked methods
  rv.value = value;
  return rv;
});

it('does stuff', () => {
  const a = new A('some-value');
  expect(A.mock.instances.length).toBe(1);
  expect(a instanceof A).toBe(true);
  expect(a).toEqual({ value: 'some-value' });
  a.setValue('another-value');
  expect(a.setValue.mock.calls.length).toBe(1);
  expect(a.setValue.mock.calls[0]).toEqual(['another-value']);
});
1
votes

The following worked for me:

A.mockImplementation(value => {
   const rv = {value: value};
   Object.setPrototypeOf(rv, A.prototype);
   return rv
})