40

I am attempting to mock a class Mailer using jest and I can't figure out how to do it. The docs don't give many examples of how this works. The process is the I will have a node event password-reset that is fired and when that event is fired, I want to send an email using Mailer.send(to, subject, body). Here is my directory structure:

project_root
-- __test__
---- server
------ services
-------- emails
---------- mailer.test.js
-- server
---- services
------ emails
-------- mailer.js
-------- __mocks__
---------- mailer.js

Here is my mock file __mocks__/mailer.js:

const Mailer = jest.genMockFromModule('Mailer');

function send(to, subject, body) {
  return { to, subject, body };
}

module.exports = Mailer;

and my mailer.test.js

const EventEmitter = require('events');
const Mailer = jest.mock('../../../../server/services/emails/mailer');

test('sends an email when the password-reset event is fired', () => {
  const send = Mailer.send();
  const event = new EventEmitter();
  event.emit('password-reset');
  expect(send).toHaveBeenCalled();
});

and finally my mailer.js class:

class Mailer {

  constructor() {
    this.mailgun = require('mailgun-js')({
      apiKey: process.env.MAILGUN_API_KEY,
      domain: process.env.MAILGUN_DOMAIN,
    });
  }

  send(to, subject, body) {
    return new Promise((reject, resolve) => {
      this.mailgun.messages().send({
        from: 'Securely App <friendly-robot@securelyapp.com>',
        to,
        subject: subject,
        html: body,
      }, (error, body) => {
        if (error) {
          return reject(error);
        }

        return resolve('The email was sent successfully!');
      });
    });
  }

}

module.exports = new Mailer();

So, how do I successfully mock and test this class, using Jest? Many thanks for helping!

dericcain
  • 2,182
  • 7
  • 30
  • 52

2 Answers2

27

You don't have to mock your mailer class but the mailgun-js module. So mailgun is a function that returns the function messages that return the function send. So the mock will look like this.

for the happy path

const happyPath = () => ({
  messages: () => ({
    send: (args, callback) => callback()
  })
})

for the error case

const errorCase = () => ({
  messages: () => ({
    send: (args, callback) => callback('someError')
  })
})

as you have this 2 cases it make sense to mock the module inside your test. First you have to mock it with a simple spy where we later can set the implementation for our cases and then we have to import the module.

jest.mock('mailgun-js', jest.fn())
import mailgun from 'mailgun-js'
import Mailer from '../../../../server/services/emails/mailer'

As your module uses promises we have 2 options either return the promise from the test or use async/await. I use the later one for more info have a look here.

test('test the happy path', async() => {
 //mock the mailgun so it returns our happy path mock
  mailgun.mockImplementation(() => happyPath)
  //we need to use async/awit here to let jest recognize the promise
  const send = await Mailer.send();
  expect(send).toBe('The email was sent successfully!')
});

If you would like to test that the mailgun send method was called with the correct parameter you need to adapt the mock like this:

const send = jest.fn((args, callback) => callback())
const happyPath = () => ({
  messages: () => ({
    send: send
  })
})

Now you could check that the first parameter for send was correct:

expect(send.mock.calls[0][0]).toMatchSnapshot()
Andreas Köberle
  • 106,652
  • 57
  • 273
  • 297
  • I can't wait to get home and try this out. Thank you so much for the super-great explanation. – dericcain May 03 '17 at 13:16
  • 1
    Tried with the same way to mock mailgun-js, but endup with receiving the error `mailgun_js_1.default is not a function` – muthu Dec 24 '19 at 06:27
17

Just for Googlers and future visitors, here's how I've setup jest mocking for ES6 classes. I also have a working example at github, with babel-jest for transpiling the ES module syntax so that jest can mock them properly.

__mocks__/MockedClass.js

const stub = {
  someMethod: jest.fn(),
  someAttribute: true
}

module.exports = () => stub;

Your code can call this with new, and in your tests you can call the function and overwrite any default implementation.

example.spec.js

const mockedClass = require("path/to/MockedClass")(); 
const AnotherClass = require("path/to/AnotherClass");
let anotherClass;

jest.mock("path/to/MockedClass");

describe("AnotherClass", () => {
  beforeEach(() => {
    mockedClass.someMethod.mockImplementation(() => {
      return { "foo": "bar" };
    });

    anotherClass = new AnotherClass();
  });

  describe("on init", () => {
    beforeEach(() => { 
      anotherClass.init(); 
    });

    it("uses a mock", () => {
      expect(mockedClass.someMethod.toHaveBeenCalled();
      expect(anotherClass.settings)
        .toEqual(expect.objectContaining({ "foo": "bar" }));
    });
  });

});
Justus Romijn
  • 15,699
  • 5
  • 51
  • 63
  • 4
    Not working for me. `mockedClass.someMethod` is `undefined`. Using jest 21.2.1. – stone Nov 22 '17 at 08:20
  • what is "path/to/YourClass"? the only clases that appear in the code are: mockedClass and AnotherClass. I guess it should be "path/to/MockedClass" instead, am I right? – Packet Tracer Nov 24 '17 at 13:53
  • 1
    @PacketTracer you are correct! I've updated the code example. – Justus Romijn Nov 26 '17 at 19:49
  • 1
    @JustusRomijn, awesome - I have been searching for a solution for this for quite some time, but returning a function that always returns the same instance is pretty smart. Thanks! – Flo Dec 04 '17 at 19:42
  • 4
    @JustusRomijn, how can you call `new` on an arrow function? I get error 'mockedClass is not a constructor' when AnotherClass calls `new mockedClass()`. – stone Jan 09 '18 at 08:54
  • Hmm well for me it works, but maybe that is because the arrow function is assigned to the exports property of the module, so it acts as a method. In that case it inherits some properties and it might become "new"-able because of this. I will clarify the code example a bit to make sure all pieces are connected properly. – Justus Romijn Jan 09 '18 at 15:29
  • 1
    I've updated my answer to point to a working example, a github repo. – Justus Romijn Jan 09 '18 at 16:38
  • 1
    babel-preset-env, which your repo is using, converts the code to ES5, which doesn't have arrow functions or classes. So it looks like this answer works for those transpiling to ES5, but not for native ES6 code. I'm working on updating the Jest docs for mocking ES6 classes and wanted to be sure I understood what was happening here. Thanks for putting that repo up! – stone Jan 09 '18 at 23:09
  • @stone I don't think mocking works for non-transpiled imports though. Because then you don't have a hook to replace the returned code with something else. By transpiling the imports, it can use some custom code to not just return the original file, but do some magic to return something else instead. – Justus Romijn Feb 04 '18 at 16:02
  • @JustusRomijn My comment is about transpiling **to ES5**. That is not related to jest's module system, which in any case is not a transpiler. – stone Feb 04 '18 at 21:32