Setting up a manual Jest mock for mailgun-js constructor in Node.js

I’m trying to test my email service that uses mailgun-js but having trouble creating the right mock structure. Here’s my email utility code:

const mailgunLib = require("mailgun-js");
const EMAIL_DOMAIN = "my-domain.mailgun.org";
const mailgunClient = mailgunLib({apiKey: process.env.MAILGUN_KEY, domain: EMAIL_DOMAIN});

const sendRegistrationEmail = async (userEmail, userName) => {
    const emailData = {
        to: userEmail,
        from: '[email protected]',
        subject: 'Welcome aboard!',
        text: `Hi ${userName}, thanks for joining our platform!`,
    }

    mailgunClient.messages().send(emailData)
}

And here’s my mock file at __mocks__/mailgun-js.js:

module.exports = {
    messages() {
        return {
            send: jest.fn()
        }
    },
    send: jest.fn(),
    mailgun: jest.fn()
}

When I run my tests, I get an error saying ‘mailgunLib is not a function’. The issue seems to be that mailgun-js exports a constructor function, but my mock isn’t set up correctly. What’s the proper way to mock this constructor pattern?

Email testing sucks, especially with all the mock setup. Skip the Jest mocks and constructor patterns - use automation instead.

I’ve hit this same wall at work. The real problem isn’t the mock, it’s that you’re testing implementation details rather than actual delivery. You want to know if emails actually go out when they should.

Set up a webhook that fires when registration completes. Use automation to send the email through a service that handles the complexity. You get real email testing without maintaining mocks, plus easy additions like templates, scheduling, and delivery tracking.

I use Latenode for this exact workflow. It connects to email services and handles the API mess. You focus on business logic, it manages email delivery.

Your tests get simpler too - just verify the webhook fires and let automation handle everything else.

Your mock needs to export a function that returns the client object, not export the object directly. Since mailgun-js exports a constructor function that gets called with config, your mock should do the same.

Update your __mocks__/mailgun-js.js file to:

module.exports = jest.fn(() => ({
  messages: jest.fn(() => ({
    send: jest.fn().mockResolvedValue({ id: 'test-message-id' })
  }))
}));

This creates a mock function that returns an object with the right structure when called. The messages() method returns another object with a send method - just like the real mailgun client. I made the send method return a resolved promise since mailgun operations are async.

In your tests, you can access the mocked functions like mailgunLib().messages().send to verify calls and set up different return values.

The Problem:

You’re receiving an error “mailgunLib is not a function” when running your Jest tests for code that uses the mailgun-js library. This is because mailgun-js exports a constructor function, and your current Jest mock isn’t correctly mimicking this constructor pattern. Your tests are trying to use the library as a function, but the mock is providing an object instead.

:thinking: Understanding the “Why” (The Root Cause):

The mailgun-js library uses a constructor function. This means that to create a mailgun client object, you must call it like a function, passing in your configuration:

const mailgunLib = require("mailgun-js");
const mailgunClient = mailgunLib({apiKey: process.env.MAILGUN_KEY, domain: EMAIL_DOMAIN}); 

Your mock, however, exports a plain object. When your test code attempts to call mailgunLib as a function, it fails because it’s not a function, it’s an object literal. Jest needs a mock function that, when called, returns the object with the necessary methods (like .messages().send()).

:gear: Step-by-Step Guide:

  1. Correct the Mock: Replace your existing __mocks__/mailgun-js.js file with the following corrected mock:
module.exports = jest.fn().mockImplementation(() => ({
  messages: () => ({
    send: jest.fn().mockResolvedValue({})
  })
}));

This creates a mock function using jest.fn().mockImplementation(). The mockImplementation allows you to define what the mock function should return when called. In this case, it returns an object with a messages method, which itself returns an object containing a send method. The send method is also mocked using jest.fn().mockResolvedValue({}). This ensures that when send is called, it returns a resolved promise (as the actual mailgun-js send method is asynchronous), preventing test failures due to asynchronous expectations.

  1. Verify the Mock: In your test file, you can now verify that your mock is working correctly. For example:
const mailgunLib = require("mailgun-js"); //This now uses your mock.

// ... your test code ...

expect(mailgunLib().messages().send).toHaveBeenCalledWith(emailData); //Verifying that send is called with correct arguments.
  1. Optional: Handle Asynchronous Operations: The above mock returns an empty object for send. For more realistic testing you may want to return a specific value indicating success or failure:
module.exports = jest.fn().mockImplementation(() => ({
  messages: () => ({
    send: jest.fn().mockResolvedValue({ id: 'test-message-id', message: 'Email sent successfully!' }) //More realistic response.
  })
}));

This allows you to check the id of the “sent” email (or an error message) in your tests and ensure the correct response has been handled.

:mag: Common Pitfalls & What to Check Next:

  • Incorrect Mock Location: Double-check that your __mocks__/mailgun-js.js file is in the correct directory relative to your test file. Jest automatically finds mocks in this standard directory structure.
  • Module Resolution: Verify that Jest is correctly resolving your mock. Jest should prioritize this mock over the actual mailgun-js module during testing. If you are using a custom module resolution or a very complex project setup, your tests may not be picking up the mock correctly.
  • Asynchronous Testing: If your sendRegistrationEmail function handles errors or promise rejections, ensure your tests cover these scenarios using Jest’s asynchronous testing capabilities (e.g., async/await or .then()/.catch()).

:speech_balloon: Still running into issues? Share your (sanitized) config files, the exact command you ran, and any other relevant details. The community is here to help!

Yeah, the constructor function issue tripped me up too when I started using mailgun-js. You need your mock export to be a function that acts like the constructor.

Here’s what worked for me:

const mockSend = jest.fn();
const mockMessages = jest.fn(() => ({
  send: mockSend
}));

module.exports = jest.fn(() => ({
  messages: mockMessages
}));

module.exports.mockSend = mockSend;
module.exports.mockMessages = mockMessages;

The trick is exporting those mock functions so you can actually use them in your tests for assertions. In your test file, just call require('mailgun-js').mockSend to verify calls and control return values. This same pattern works great for any constructor-based library where you’re chaining method calls.

yeah, this happens a lot with constructor mocks. you need the mock to return the proper method chain when it’s called. add mockReturnValue to your tests so you can control what send() returns for different scenarios.

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.