npm install -D vitest
Use vi.mock, vi.spyOn, and mockReturnValue to isolate units from external dependencies.
1 import { describe, it, expect, vi, beforeEach } from 'vitest'; 2 3 // Module mock — replaces entire module 4 vi.mock('./mailer', () => ({ 5 sendEmail: vi.fn(), 6 sendSMS: vi.fn(), 7 })); 8 9 import { sendEmail, sendSMS } from './mailer'; 10 import { NotificationService } from './notification-service'; 11 12 describe('NotificationService', () => { 13 let service: NotificationService; 14 15 beforeEach(() => { 16 vi.clearAllMocks(); // Reset call history, keep mock implementations 17 service = new NotificationService(); 18 }); 19 20 it('sends email notification', async () => { 21 vi.mocked(sendEmail).mockResolvedValue({ messageId: 'msg-1' }); 22 23 await service.notifyUser({ 24 userId: 'u-1', 25 type: 'welcome', 26 email: 'alice@example.com', 27 }); 28 29 expect(sendEmail).toHaveBeenCalledWith({ 30 to: 'alice@example.com', 31 subject: expect.stringContaining('Welcome'), 32 html: expect.any(String), 33 }); 34 expect(sendEmail).toHaveBeenCalledOnce(); 35 }); 36 37 it('retries on failure then succeeds', async () => { 38 vi.mocked(sendEmail) 39 .mockRejectedValueOnce(new Error('SMTP timeout')) 40 .mockResolvedValueOnce({ messageId: 'msg-2' }); // Second call succeeds 41 42 await service.notifyUser({ 43 userId: 'u-2', 44 type: 'password-reset', 45 email: 'bob@example.com', 46 }); 47 48 expect(sendEmail).toHaveBeenCalledTimes(2); // Retried once 49 }); 50 51 it('throws after max retries exhausted', async () => { 52 vi.mocked(sendEmail).mockRejectedValue(new Error('SMTP down')); 53 54 await expect( 55 service.notifyUser({ userId: 'u-3', type: 'welcome', email: 'x@x.com' }) 56 ).rejects.toThrow('Failed to send notification after 3 attempts'); 57 58 expect(sendEmail).toHaveBeenCalledTimes(3); 59 }); 60 }); 61 62 // Minimal NotificationService for test 63 class NotificationService { 64 async notifyUser(payload: { userId: string; type: string; email: string }) { 65 const maxRetries = 3; 66 for (let attempt = 0; attempt < maxRetries; attempt++) { 67 try { 68 await sendEmail({ 69 to: payload.email, 70 subject: `Welcome to our app`, 71 html: `<p>Hi!</p>`, 72 }); 73 return; 74 } catch (err) { 75 if (attempt === maxRetries - 1) { 76 throw new Error(`Failed to send notification after ${maxRetries} attempts`); 77 } 78 } 79 } 80 } 81 }
All mock interactions verified; retry logic tested without real HTTP calls
Sign in to share your feedback and join the discussion.