32

I'm a newbie to Jest. I've managed to mock my own stuff, but seem to be stuck mocking a module. Specifically constructors.

usage.js

const AWS = require("aws-sdk")
cw = new AWS.CloudWatch({apiVersion: "2010-08-01"})
...
function myMetrics(params) { 
  cw.putMetricData(params, function(err, data){})
}

I'd like to do something like this in the tests.

const AWS = jest.mock("aws-sdk")
class FakeMetrics {
  constructor() {}
  putMetricData(foo,callback) {
    callback(null, "yay!")
  }
}

AWS.CloudWatch = jest.fn( (props) => new FakeMetrics())

However when I come to use it in usage.js the cw is a mockConstructor not a FakeMetrics

I realise that my approach might be 'less than idiomatic' so I'd be greatful for any pointers.

This is a minimal example https://github.com/ollyjshaw/jest_constructor_so

npm install -g jest

jest

Oliver Shaw
  • 4,390
  • 4
  • 22
  • 34

3 Answers3

34

Above answer works. However, after some time working with jest I would just use the mockImplementation functionality which is useful for mocking constructors.

Below code could be an example:

import * as AWS from 'aws-sdk';

jest.mock('aws-sdk', ()=> {
    return {
        CloudWatch : jest.fn().mockImplementation(() => { return {} })
    }
});

test('AWS.CloudWatch is called', () => {
    new AWS.CloudWatch();
    expect(AWS.CloudWatch).toHaveBeenCalledTimes(1);
});

Note that in the example the new CloudWatch() just returns an empty object.

mikemaccana
  • 81,787
  • 73
  • 317
  • 396
kimy82
  • 2,566
  • 18
  • 22
  • This would leak to other code using the sdk right? Say I'm using CognitoIdentityServiceProvider in some other place, my tests for that would fail since the mock is not there. How do you reset this on a per test module basis? – Alejandro Corredor Aug 05 '20 at 18:17
21

The problem is how a module is being mocked. As the reference states,

Mocks a module with an auto-mocked version when it is being required. <...> Returns the jest object for chaining.

AWS is not module object but jest object, and assigning AWS.CloudFormation will affect nothing.

Also, it's CloudWatch in one place and CloudFormation in another.

Testing framework doesn't require to reinvent mock functions, they are already there. It should be something like:

const AWS = require("aws-sdk");
const fakePutMetricData = jest.fn()
const FakeCloudWatch = jest.fn(() => ({
    putMetricData: fakePutMetricData
}));                        
AWS.CloudWatch = FakeCloudWatch;

And asserted like:

expect(fakePutMetricData).toHaveBeenCalledTimes(1);
Estus Flask
  • 150,909
  • 47
  • 291
  • 441
  • My confusion was certainly this bit. "AWS is not module object but jest object, and assigning AWS.CloudFormation will affect nothing." – Oliver Shaw Dec 02 '17 at 13:05
  • 1
    How would you handle this if `AWS` was a class with static variables? – David Schumann Jun 25 '18 at 15:34
  • @DavidNathan Possibly with jest.spyOn. This depends on your case. It's unclear what are 'static variables'. Do you mean static methods? If you have a specific case in mind, consider asking a question that reflects your problem. – Estus Flask Jun 25 '18 at 15:37
  • Sorry. I meant class variables. I opened a separate question: https://stackoverflow.com/questions/51027294/how-to-mock-a-module-exporting-a-class-with-class-variables – David Schumann Jun 25 '18 at 15:40
7

According to the documentation mockImplementation can also be used to mock class constructors:

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

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

const some = new SomeClass();
some.method('a', 'b');
console.log('Calls to method: ', mockMethod.mock.calls);

If your class constructor has parameters, you could pass jest.fn() as an argument (eg. const some = new SomeClass(jest.fn(), jest.fn());

David
  • 2,089
  • 1
  • 18
  • 31
  • Could you give an explicit example where constructor arguments are used to set properties for the class instance? I don't understand how to make it work so the end object has the expected properties. You mention passing jest.fn() as an argument, but where? How does it work? – Ariane May 27 '21 at 13:26
  • I updated my answer. However, if you need to set properties for the class instance, then mocking is probably not what you want anyway? – David May 27 '21 at 15:05
  • Well, there's code that needs specific instance properties to exist, and getting the actual class isn't possible because its contents depend on specific Webpack loaders that I couldn't replicate. I ended up creating a central mock in __mocks__ that's an actual amended class instead of a jest.fn(), and then enabling it with a simple jest.mock('path'). Only way I found to both have properties and obey constraints where they need to actually be class instances. – Ariane May 28 '21 at 17:15