Jest重置Node模块Mock问题:GCF测试中SendGrid Mock失效
I’ve run into this exact problem before when testing cloud functions that rely on external services like SendGrid! The issue is that once you set a static mock resolution for @sendgrid/mail’s send method, Jest keeps that implementation across all test cases unless you explicitly override it. Here’s how to fix it with a couple of reliable approaches:
Approach 1: Use a Factory Function with a Mockable send Method
Instead of locking in a fixed resolved value when mocking the module, define the send method as a generic Jest mock function. This lets you adjust its return value per test case:
// At the top of your test file jest.mock('@sendgrid/mail', () => ({ send: jest.fn(), // Leave this as a blank mock function setApiKey: jest.fn(), // Mock any other required methods your function uses })); import sgMail from '@sendgrid/mail'; import { contactSupportByMail } from './index'; describe('contactSupportByMail', () => { // Reset mock calls between tests to avoid cross-test pollution beforeEach(() => { jest.clearAllMocks(); }); it('resolves successfully when SendGrid sends the email', async () => { // Set a successful resolution for this test only (sgMail.send as jest.Mock).mockResolvedValueOnce({ statusCode: 202 }); const response = await contactSupportByMail({ // Insert your test payload here email: 'test@example.com', message: 'Hello support!' }); expect(sgMail.send).toHaveBeenCalledTimes(1); expect(response).toBe('success'); // Adjust based on your function's return }); it('handles SendGrid errors gracefully', async () => { // Override the mock to reject for this error scenario (sgMail.send as jest.Mock).mockRejectedValueOnce(new Error('SendGrid API failed')); // Verify the function throws/catches the error as expected await expect(contactSupportByMail({ email: 'test@example.com', message: 'Hello support!' })).rejects.toThrow('SendGrid API failed'); expect(sgMail.send).toHaveBeenCalledTimes(1); }); });
The key here is using mockResolvedValueOnce and mockRejectedValueOnce to define per-test behavior, instead of a permanent mock resolution. The as jest.Mock type assertion is needed because TypeScript doesn’t know we’ve mocked the send method.
Approach 2: Use jest.spyOn for Granular Control
If you prefer not to mock the entire module upfront, you can spy on the send method directly:
import sgMail from '@sendgrid/mail'; import { contactSupportByMail } from './index'; describe('contactSupportByMail', () => { let sendSpy: jest.SpyInstance; beforeEach(() => { // Spy on the send method and replace it with a mock sendSpy = jest.spyOn(sgMail, 'send').mockImplementation(() => Promise.resolve({ statusCode: 202 })); // Mock setApiKey if your function calls it jest.spyOn(sgMail, 'setApiKey').mockImplementation(() => {}); }); afterEach(() => { // Restore the original send method after each test sendSpy.mockRestore(); }); it('handles SendGrid failures', async () => { // Override the spy to reject for this test sendSpy.mockRejectedValueOnce(new Error('API error')); await expect(contactSupportByMail({ /* test data */ })).rejects.toThrow('API error'); }); });
Why Your Original Setup Failed
If you initially mocked send with mockResolvedValue (not mockResolvedValueOnce), Jest keeps that resolved value for all subsequent tests. jest.clearAllMocks() only clears call history—it doesn’t reset the mock’s return behavior. Using the "once" variants or redefining the mock per test fixes this.
Make sure to also mock any other SendGrid methods your function uses (like setApiKey) to avoid runtime errors during testing.
内容的提问来源于stack exchange,提问作者Jesús Fuentes

