如何用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模块,不用修改业务代码:
- 模拟
config模块,返回测试用配置值:
jest.mock('../config', () => ({ mailApiKey: 'test-key', mailApiUrl: 'test-url', mailDomain: 'test-domain', senderEmail: 'test-sender@example.com' }));
- 模拟
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 } }) })); });
- 编写测试用例,调用
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实例和配置的依赖注入进去,避免硬编码实例化:
- 重构
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; };
- 测试时直接替换客户端为模拟对象:
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
相关产品推荐
相关产品推荐

