typescriptjestjsmockingmailgun

mocking mailgun.js with jest/typescript


I've got this emailService module in my project:

import config from "../config";
import formData from "form-data";
import Mailgun from "mailgun.js";
import MailOptions from "../@types/MailOptions";

const mailgun = new Mailgun(formData);
const mg = mailgun.client({
  username: "api",
  key: config.mailApiKey,
  url: config.mailApiUrl,
});

export const sendMail = (mailOptions: MailOptions): Promise<unknown> => {
  return new Promise((resolve, reject) => {
    mg.messages
      .create(config.mailDomain, mailOptions)
      .then((msg) => resolve(msg))
      .catch((err) => reject(err));
  });
};

export const setMailOptions = (
  recipientEmail: string,
  subject: string,
  body: string
): MailOptions => {
  const mailOptions: MailOptions = {
    from: config.senderEmail,
    to: recipientEmail,
    subject: subject,
    html: body,
  };

  return mailOptions;
};

Normally I import this emailService.ts in any other module I want to send emails in.

I'm trying to figure out how to test the sendMail() in emailService, and assert that mg.messages.create() was called.

I have a feeling I need to refactor this code so that I can mock the config values and Mailgun instance being used by emailService, unless there's an alternative way to mock Mailgun in the test suite in such a way that that the emailService would use it?


Solution

  • You'll need to export your mailgun client instance in order to mock it.

    mailgun.ts:

    import config from "../config";
    import formData from "form-data";
    import Mailgun from "mailgun.js";
    
    const mailgun = new Mailgun(formData);
    const mg = mailgun.client({
      username: "api",
      key: config.mailApiKey,
      url: config.mailApiUrl,
    });
    
    export default mg;
    

    mailgunMock.ts:

    import { mockDeep, DeepMockProxy } from 'jest-mock-extended';
    import Client from 'mailgun.js/client';
    import mg from '../mailgun';
    
    jest.mock('../mailgun', () => ({
        __esModule: true,
        default: mockDeep<Client>(),
    }));
    
    export const mailgunMock = mg as unknown as DeepMockProxy<Client>;
    

    You can then use this mock in your test suite:

    emailService.test.ts:

    import { mailgunMock } from './mocks/mailgunMock';
    import { sendMail } from '../emailService';
    
    describe('send email', () => {
    
         const emailMessage = {
                id: 'test',
                message: 'test',
                status: 200,
                details: 'n/a',
            };
    
         test('setup mocks', async () => {
              mailgunMock.messages.create.mockResolvedValue(emailMessage);
         });
    
         test('should send email', async () => {
              await expect(
                   sendMail({from: 'test', to: 'test', subject: 'test', html: 'test'}),
              ).resolves.toEqual(emailMessage);
    
              expect(mailgunMock.messages.create.mock.calls[0][1]).toEqual(
                    expect.objectContaining({
                        from: 'test',
                        to: 'test',
                        subject: 'test',
                        html: 'test',
                    }),
                );
         });
    });