Careeroza
Mocking Providers
Ways to Replace a Dependency
| Technique | Code |
|---|---|
| Value mock | { provide: PaymentGateway, useValue: { charge: jest.fn() } } |
| Fake class | { provide: PaymentGateway, useClass: FakeGateway } |
| Override in e2e | .overrideProvider(PaymentGateway).useValue(fake) |
| Override a guard | .overrideGuard(JwtAuthGuard).useValue({ canActivate: () => true }) |
| Auto-mock everything | .useMocker(createMock) (from @golevelup/ts-jest) |
src/orders/orders.service.spec.ts
describe('OrdersService.place', () => {
const payments = { charge: jest.fn() };
const notifications = { orderPlaced: jest.fn() };
const repo = { save: jest.fn((o) => ({ id: 'ORD-1', ...o })) };
let service: OrdersService;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
OrdersService,
{ provide: PaymentGateway, useValue: payments },
{ provide: NotificationsService, useValue: notifications },
{ provide: OrdersRepository, useValue: repo },
],
}).compile();
service = module.get(OrdersService);
jest.clearAllMocks();
});
it('does not notify when payment fails', async () => {
payments.charge.mockRejectedValue(new Error('card declined'));
await expect(service.place({ productId: 7, quantity: 1 })).rejects.toThrow('card declined');
expect(notifications.orderPlaced).not.toHaveBeenCalled();
});
});What jest.fn() Really Is
Example
A mock function records every call and returns whatever you program it to return. A tiny version, used to test an OrdersService:
type Mock<A extends unknown[], R> = ((...args: A) => R) & { calls: A[]; mockReturnValue(v: R): void };
function fn<A extends unknown[], R>(impl?: (...args: A) => R): Mock<A, R> {
let fixed: { v: R } | undefined;
const mock = ((...args: A) => {
mock.calls.push(args);
if (fixed) return fixed.v;
return impl ? impl(...args) : (undefined as R);
}) as Mock<A, R>;
mock.calls = [];
mock.mockReturnValue = (v: R) => { fixed = { v }; };
return mock;
}
class OrdersService {
constructor(private payments: { charge(amount: number): boolean }, private sms: { send(phone: string, text: string): void }) {}
place(amount: number, phone: string) {
if (!this.payments.charge(amount)) return 'PAYMENT_FAILED';
this.sms.send(phone, 'Order confirmed');
return 'PLACED';
}
}
function test(name: string, body: () => void) {
try { body(); console.log(' ✓', name); } catch (e) { console.log(' ✗', name, '-', (e as Error).message); }
}
const expectEq = (a: unknown, b: unknown) => { if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(JSON.stringify(a) + ' !== ' + JSON.stringify(b)); };
console.log('OrdersService');
test('sends SMS after successful payment', () => {
const charge = fn((_amount: number) => true);
const send = fn((_phone: string, _text: string) => {});
expectEq(new OrdersService({ charge }, { send }).place(1299, '+919876543210'), 'PLACED');
expectEq(send.calls, [['+919876543210', 'Order confirmed']]);
});
test('does not send SMS when payment fails', () => {
const charge = fn((_amount: number) => true);
charge.mockReturnValue(false);
const send = fn((_phone: string, _text: string) => {});
expectEq(new OrdersService({ charge }, { send }).place(1299, '+919876543210'), 'PAYMENT_FAILED');
expectEq(send.calls.length, 0);
});Output
OrdersService ✓ sends SMS after successful payment ✓ does not send SMS when payment fails