45

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 <[email protected]>',
        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!

2 Answers 2

28

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()
Sign up to request clarification or add additional context in comments.

2 Comments

I can't wait to get home and try this out. Thank you so much for the super-great explanation.
Tried with the same way to mock mailgun-js, but endup with receiving the error mailgun_js_1.default is not a function
19

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

});

10 Comments

Not working for me. mockedClass.someMethod is undefined. Using jest 21.2.1.
@PacketTracer you are correct! I've updated the code example.
@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!
@JustusRomijn, how can you call new on an arrow function? I get error 'mockedClass is not a constructor' when AnotherClass calls new mockedClass().
I've updated my answer to point to a working example, a github repo.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.