You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Jest/TypeScript模拟Mailgun,测试emailService的sendMail方法并断言调用

问题描述

我的项目中有如下emailService模块:

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

我通常会在需要发送邮件的模块中导入emailService.ts。现在我想测试sendMail()方法,断言mg.messages.create()是否被调用,但不确定是要重构代码来模拟配置值和Mailgun实例,还是有其他方法在测试套件中直接模拟Mailgun供emailService使用?

解决方案

你有两种可行思路,不需要纠结必须重构:

方案一:不重构代码,直接依赖测试框架模拟模块

如果用Jest这类测试框架,可以直接模拟mailgun.js模块和你的config模块,不用修改业务代码:

  1. 模拟config模块,返回测试用配置值:
jest.mock('../config', () => ({
  mailApiKey: 'test-key',
  mailApiUrl: 'test-url',
  mailDomain: 'test-domain',
  senderEmail: 'test-sender@example.com'
}));
  1. 模拟mailgun.js的client方法和messages.create方法,捕获调用情况:
const mockCreate = jest.fn().mockResolvedValue({ id: 'test-msg-id' });
jest.mock('mailgun.js', () => {
  return jest.fn().mockImplementation(() => ({
    client: jest.fn().mockReturnValue({
      messages: {
        create: mockCreate
      }
    })
  }));
});
  1. 编写测试用例,调用sendMail后断言mockCreate是否被正确调用:
import { sendMail, setMailOptions } from './emailService';

describe('sendMail', () => {
  it('should call mg.messages.create with correct params', async () => {
    const testOptions = setMailOptions('test@example.com', 'Test Subject', '<p>Test Body</p>');
    await sendMail(testOptions);
    
    expect(mockCreate).toHaveBeenCalledWith('test-domain', testOptions);
  });
});

方案二:重构代码,提升可测试性(可选)

如果想让代码本身更易测试,可以把Mailgun实例和配置的依赖注入进去,避免硬编码实例化:

  1. 重构emailService,把mg实例的创建逻辑解耦:
import config from "../config";
import formData from "form-data";
import Mailgun from "mailgun.js";
import MailOptions from "../@types/MailOptions";

// 导出创建Mailgun客户端的方法,方便测试时替换
export const createMailgunClient = () => {
  const mailgun = new Mailgun(formData);
  return mailgun.client({
    username: "api",
    key: config.mailApiKey,
    url: config.mailApiUrl,
  });
};

// 默认使用默认客户端
let mg = createMailgunClient();

// 允许外部替换客户端(测试用)
export const setMailgunClient = (client: typeof mg) => {
  mg = client;
};

export const sendMail = (mailOptions: MailOptions): Promise<unknown> => {
  // 直接返回Promise,无需额外包装
  return mg.messages.create(config.mailDomain, mailOptions);
};

// setMailOptions方法保持不变
export const setMailOptions = (
  recipientEmail: string,
  subject: string,
  body: string
): MailOptions => {
  const mailOptions: MailOptions = {
    from: config.senderEmail,
    to: recipientEmail,
    subject: subject,
    html: body,
  };

  return mailOptions;
};
  1. 测试时直接替换客户端为模拟对象:
import { sendMail, setMailgunClient, setMailOptions } from './emailService';

describe('sendMail', () => {
  it('should call messages.create with correct params', async () => {
    const mockCreate = jest.fn().mockResolvedValue({ id: 'test-msg-id' });
    const mockClient = {
      messages: {
        create: mockCreate
      }
    };
    
    // 设置模拟客户端
    setMailgunClient(mockClient);
    
    const testOptions = setMailOptions('test@example.com', 'Test Subject', '<p>Test Body</p>');
    await sendMail(testOptions);
    
    expect(mockCreate).toHaveBeenCalledWith('test-domain', testOptions);
  });
});

总结

  • 如果不想改动现有代码,方案一的模块模拟完全可以实现测试需求;
  • 如果希望代码的可维护性和可测试性更好,方案二的重构是更优选择,同时也让依赖关系更清晰。

内容的提问来源于stack exchange,提问作者Andrew Johns

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 22:42:42