node.jsunit-testingmockingjestjsnodemailer

mock nodemailer.createTransport.sendMail with jest


I have some code which uses the nodemailer module.

In the router (router.js), I have

const transporter = nodeMailer.createTransport(emailArgs);

Then inside the route (/login) I have:

...
return transporter.sendMail(mailOptions);

I'm trying to test this route using the jest testing framework. I'm having some trouble mocking out the call to sendMail. I read this nice blogpost about how to use jest mocking, but I'm getting this error:

TypeError: Cannot read property 'sendMail' of undefined

And indeed when I check the value of transporter it's undefined.

Here is my testing code (which doesn't work):

import request from "supertest";
import router from "./router";

jest.mock("nodemailer");

describe("", () => {
...

    test("", async () => {
        // 1 - 200 status code; 2 - check email was sent
        expect.assertions(2);

        const response = await request(router)
            .post("/login")
            // global variable
            .send({ "email": email })
            .set("Accept", "application/json")
            .expect("Content-Type", /json/);

        // should complete successfully
        expect(response.status).toBe(200);
        // TODO not sure how to express the expect statement here
    });
});

So my question is how do I mock out a method of an instance of a class which is returned by a module?


Solution

  • I ran into the same problem and found a solution. Here is what I've discovered:

    With jest.mock("nodemailer"); you tell jest to replace nodemailer with an auto-mock. This means every property of nodemailer is replaced with an empty mock function (similar to jest.fn()).

    That is the reason why you get the error TypeError: Cannot read property 'sendMail' of undefined. In order to have something useful, you have to define the mock function of nodemailer.createTransport.

    In our case we wan't to have an object with a property sendMail. We could do this with nodemailer.createTransport.mockReturnValue({"sendMail": jest.fn()});. Since you may want to test if sendMail was called, it is a good idea to create that mock function before hand.

    Here is a complete example of your testing code:

    import request from "supertest";
    import router from "./router";
    
    const sendMailMock = jest.fn(); // this will return undefined if .sendMail() is called
    
    // In order to return a specific value you can use this instead
    // const sendMailMock = jest.fn().mockReturnValue(/* Whatever you would expect as return value */);
    
    jest.mock("nodemailer");
    
    const nodemailer = require("nodemailer"); //doesn't work with import. idk why
    nodemailer.createTransport.mockReturnValue({"sendMail": sendMailMock});
    
    beforeEach( () => {
        sendMailMock.mockClear();
        nodemailer.createTransport.mockClear();
    });
    
    describe("", () => {
    ...
    
        test("", async () => {
            // 1 - 200 status code; 2 - check email was sent
            expect.assertions(2);
    
            const response = await request(router)
                .post("/login")
                // global variable
                .send({ "email": email })
                .set("Accept", "application/json")
                .expect("Content-Type", /json/);
    
            // should complete successfully
            expect(response.status).toBe(200);
    
            // TODO not sure how to express the expect statement here
            expect(sendMailMock).toHaveBeenCalled();
        });
    });