How to correctly mock an ES6 class in Jest?

What is the correct way to mock an ES6 class using jest mock class?

I am trying to mock a Mailer class in Jest, but I’m running into some issues. The documentation doesn’t provide many examples on how to mock a class like this. Here’s the scenario: I have a Node.js event password-reset that is triggered, and when this 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

This 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 in mailer.test.js, I am trying to mock the send method:

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

Lastly, my mailer.js class looks like this:

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,
        html: body,
      }, (error, body) => {
        if (error) {
          return reject(error);
        }
        return resolve('The email was sent successfully!');
      });
    });
  }
}

module.exports = new Mailer();

What is the correct way to successfully mock and test this Mailer class using Jest?

Been doing this for a while, and trust me, you don’t need to mock the whole class—just the module it depends on.

If your class uses mailgun-js, you should mock the module, not the class itself. The mailgun object is a function that returns another object with a messages method, which has a send method.

Here’s how you mock different scenarios:

:white_check_mark: Happy path:

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

:x: Error case:

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

Now, mock mailgun-js in your tests:

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

test('test the happy path', async () => {
  mailgun.mockImplementation(() => happyPath());
  const send = await Mailer.send('test@example.com', 'Subject', 'Body');
  expect(send).toBe('The email was sent successfully!');
});

Want to check if send was called with the right args? Try:

const send = jest.fn((args, callback) => callback());
expect(send.mock.calls[0][0]).toMatchSnapshot();

This is the easiest way to use jest mock class without overcomplicating things. :fire:

Great approach, But if you want a cleaner way to mock without implementing custom functions inside the test, you can mock return values directly using jest.mock().

This makes the setup reusable across multiple tests:

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

mailgun.mockReturnValue({
  messages: () => ({
    send: jest.fn().mockImplementation((args, callback) => callback())
  })
});

Now, the test becomes much simpler:

test('successful email send', async () => {
  const result = await Mailer.send('to@example.com', 'Subject', 'Body');
  expect(result).toBe('The email was sent successfully!');
});

This approach makes your jest mock class setup much cleaner while keeping the focus on testing. :white_check_mark:

Both solid methods! But if you don’t want to mock the entire module, a great alternative is spying on the method directly using jest.spyOn().

This lets you track calls to send without messing with the implementation:

import Mailer from '../../../../server/services/emails/mailer';

test('email sent successfully', async () => {
  const sendSpy = jest.spyOn(Mailer, 'send').mockResolvedValue('The email was sent successfully!');
  
  const result = await Mailer.send('to@example.com', 'Subject', 'Body');
  
  expect(result).toBe('The email was sent successfully!');
  expect(sendSpy).toHaveBeenCalledWith('to@example.com', 'Subject', 'Body');
  
  sendSpy.mockRestore(); // Always clean up after!
});

With this approach, you get precise control while keeping the jest mock class implementation minimal. :dart: