4.5 KiB
4.5 KiB
title, impact, impactDescription, tags
| title | impact | impactDescription | tags |
|---|---|---|---|
| Mock External Services in Tests | HIGH | Ensures fast, reliable, deterministic tests | testing, mocking, external-services, jest |
Mock External Services in Tests
Never call real external services (APIs, databases, message queues) in unit tests. Mock them to ensure tests are fast, deterministic, and don't incur costs. Use realistic mock data and test edge cases like timeouts and errors.
Incorrect (calling real APIs and databases):
// Call real APIs in tests
describe('PaymentService', () => {
it('should process payment', async () => {
const service = new PaymentService(new StripeClient(realApiKey));
// Hits real Stripe API!
const result = await service.charge('tok_visa', 1000);
// Slow, costs money, flaky
});
});
// Use real database
describe('UsersService', () => {
beforeEach(async () => {
await connection.query('DELETE FROM users'); // Modifies real DB
});
it('should create user', async () => {
const user = await service.create({ email: 'test@test.com' });
// Side effects on shared database
});
});
// Incomplete mocks
const mockHttpService = {
get: jest.fn().mockResolvedValue({ data: {} }),
// Missing error scenarios, missing other methods
};
Correct (mock all external dependencies):
// Mock HTTP service properly
describe('WeatherService', () => {
let service: WeatherService;
let httpService: jest.Mocked<HttpService>;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
WeatherService,
{
provide: HttpService,
useValue: {
get: jest.fn(),
post: jest.fn(),
},
},
],
}).compile();
service = module.get(WeatherService);
httpService = module.get(HttpService);
});
it('should return weather data', async () => {
const mockResponse = {
data: { temperature: 72, humidity: 45 },
status: 200,
statusText: 'OK',
headers: {},
config: {},
};
httpService.get.mockReturnValue(of(mockResponse));
const result = await service.getWeather('NYC');
expect(result).toEqual({ temperature: 72, humidity: 45 });
});
it('should handle API timeout', async () => {
httpService.get.mockReturnValue(
throwError(() => new Error('ETIMEDOUT')),
);
await expect(service.getWeather('NYC')).rejects.toThrow('Weather service unavailable');
});
it('should handle rate limiting', async () => {
httpService.get.mockReturnValue(
throwError(() => ({
response: { status: 429, data: { message: 'Rate limited' } },
})),
);
await expect(service.getWeather('NYC')).rejects.toThrow(TooManyRequestsException);
});
});
// Mock repository instead of database
describe('UsersService', () => {
let service: UsersService;
let repo: jest.Mocked<Repository<User>>;
beforeEach(async () => {
const mockRepo = {
find: jest.fn(),
findOne: jest.fn(),
save: jest.fn(),
delete: jest.fn(),
createQueryBuilder: jest.fn(),
};
const module = await Test.createTestingModule({
providers: [
UsersService,
{ provide: getRepositoryToken(User), useValue: mockRepo },
],
}).compile();
service = module.get(UsersService);
repo = module.get(getRepositoryToken(User));
});
it('should find user by id', async () => {
const mockUser = { id: '1', name: 'John', email: 'john@test.com' };
repo.findOne.mockResolvedValue(mockUser);
const result = await service.findById('1');
expect(result).toEqual(mockUser);
expect(repo.findOne).toHaveBeenCalledWith({ where: { id: '1' } });
});
});
// Create mock factory for complex SDKs
function createMockStripe(): jest.Mocked<Stripe> {
return {
paymentIntents: {
create: jest.fn(),
retrieve: jest.fn(),
confirm: jest.fn(),
cancel: jest.fn(),
},
customers: {
create: jest.fn(),
retrieve: jest.fn(),
},
} as any;
}
// Mock time for time-dependent tests
describe('TokenService', () => {
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2024-01-15'));
});
afterEach(() => {
jest.useRealTimers();
});
it('should expire token after 1 hour', async () => {
const token = await service.createToken();
// Fast-forward time
jest.advanceTimersByTime(61 * 60 * 1000);
expect(await service.isValid(token)).toBe(false);
});
});
Reference: Jest Mocking