Skip to Content
Course content

71: Mocking Typed Dependencies

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you're writing tests for a service that depends on other classes or APIs, you'll quickly hit a wall where TypeScript starts complaining that your mock objects aren't "compatible" with the interfaces they're replacing. It's frustrating because, in a test, you usually only care about one or two methods, not the entire API surface.

How do I mock an interface without implementing every single method?

If you have a massive interface—say, a DatabaseClient with 50 methods—you definitely don't want to manually write 50 dummy functions just to satisfy the compiler. The trick I use is to create a mock object and cast it using jest.Mocked<T> (or ts-jest's utilities).

Let's say we have a PaymentGateway interface. We only care about charge() for our current test, but the interface has a dozen other methods for refunds, disputes, and reporting.

interface PaymentGateway {
  charge(amount: number, currency: string): Promise<string>;
  refund(transactionId: string): Promise<void>;
  getAccountBalance(): Promise<number>;
  // ... 10 other methods
}

class CheckoutService {
  constructor(private gateway: PaymentGateway) {}

  async processOrder(amount: number) {
    return await this.gateway.charge(amount, 'USD');
  }
}

Instead of implementing the whole thing, you can do this:

const mockGateway = {
  charge: jest.fn(),
} as unknown as jest.Mocked<PaymentGateway>;

const service = new CheckoutService(mockGateway);
mockGateway.charge.mockResolvedValue('tx_123');

await service.processOrder(100);
expect(mockGateway.charge).toHaveBeenCalledWith(100, 'USD');

By casting to unknown first and then to jest.Mocked<PaymentGateway>, you tell TypeScript: "Trust me, this object will behave like the interface, and I want the mock-specific methods (like mockResolvedValue) available on its properties."

Why should I use jest.mocked() instead of just casting to 'any'?

I see a lot of developers just use as any to get the tests to pass. Don't do that. Using any essentially turns off the compiler for that object. If you rename a method in your interface or change a parameter from a string to a number, your any cast will keep the tests "green" even though the code is now broken.

Using jest.mocked() (the helper function) keeps the type safety intact. It wraps the mock and ensures that the arguments you pass to mockReturnValue or mockResolvedValue actually match the types defined in the original interface.

import { mocked } from 'ts-jest'; // or use the built-in jest.mocked in newer versions

const mockGateway = {
  charge: jest.fn(),
} as unknown as PaymentGateway;

const typedGateway = jest.mocked(mockGateway);

// TypeScript will error here if charge() is supposed to return a string
typedGateway.charge.mockResolvedValue(404); // Error: Argument of type 'number' is not assignable to 'string'

What if the dependency is a concrete class instead of an interface?

Mocking a class is slightly different because you're dealing with a constructor. If you're using jest.mock('./path-to-class'), Jest automatically replaces the class with a mock. However, TypeScript doesn't automatically know that the imported class is now a mock.

I usually handle this by importing the class and then casting the instance. If you're mocking a third-party library (like axios or a database driver), the pattern looks like this:

import { ExternalLogger } from './logger';
jest.mock('./logger');

const loggerInstance = new ExternalLogger();
const mockedLogger = jest.mocked(loggerInstance);

mockedLogger.log.mockImplementation(() => {
  console.log('Mock logger called!');
});

One thing to watch out for: if the class has complex internal state or private methods that the service relies on, you might find it easier to extract an interface for that class. I've spent way too many hours fighting class prototypes when a simple interface would have solved the problem in five minutes.




📋 Practical Task

Exercise: Mocking the NotificationDispatcher

You are working on a UserRegistrationService that depends on a NotificationDispatcher. The dispatcher has a massive interface with methods for SMS, Email, Push notifications, and Slack integration. You need to test that when a user registers, the sendWelcomeEmail method is called with the correct email address.

Your Task:

  • Create a mock for the NotificationDispatcher interface without implementing all its methods.
  • Ensure the mock is properly typed so you can use mockResolvedValue.
  • Write a test case that verifies UserRegistrationService.register calls sendWelcomeEmail exactly once with the provided email.
// The provided code to work with:
interface NotificationDispatcher {
  sendWelcomeEmail(email: string): Promise<boolean>;
  sendSms(phone: string, msg: string): Promise<void>;
  sendPushNotification(userId: string, msg: string): Promise<void>;
  sendSlackAlert(channel: string, msg: string): Promise<void>;
  verifyRecipient(email: string): Promise<boolean>;
}

class UserRegistrationService {
  constructor(private dispatcher: NotificationDispatcher) {}

  async register(email: string) {
    // Registration logic...
    return await this.dispatcher.sendWelcomeEmail(email);
  }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.