Lesson 4
Mocking Dependencies with Jest in TDD
Introduction to Mocks

In our journey through isolating dependencies with test doubles, we've explored dummies, stubs, and spies. This lesson focuses on mocks, which are powerful test doubles capable of simulating external dependencies in software tests. You may have noticed in the previous unit how spying on the real implementation can be messy (in the form of many console logs). Mocks can imitate the behavior of complex systems, allowing us to test code in isolation without relying on the real and sometimes unpredictable systems like databases or web services.

Now, let's review the TDD workflow:

  • Red: Write a failing test.
  • Green: Write the minimum code to pass the test.
  • Refactor: Improve the code structure without changing its behavior.

We'll demonstrate these principles using mocks, helping you to effectively isolate and test your application logic.

Why Use Mocks in TDD?

Mocks are indispensable in TDD because they allow you to test your code independently of the parts of the system you don't control. For instance, when writing tests for a PricingService, you don't want tests to fail because an external currency conversion API goes down or changes unexpectedly. Mocks provide a controlled environment where you can simulate various conditions and responses as well as validate the calls.

Mocks, unlike spies, fully simulate the dependencies rather than simply observing their behavior. A mock creates a controlled substitute for a dependency, so the actual code or functionality isn’t executed. For instance, if a function interacts with an external API, a mock can simulate different responses from that API without making a network request.

Mocking Fundamentals with Jest

Let's dive into mocking with Jest. We'll start with the basics: how to mock a module and its functions.

Consider the ExchangeRateService, which fetches exchange rates from an API. In testing the PricingService, we need to mock this service to ensure our tests don't rely on actual API responses.

Here's a simple way to mock with Jest:

TypeScript
1// In pricing-service.test.ts 2import { PricingService } from '../src/pricing-service'; 3import { ExchangeRateService } from '../src/exchange-rate-service'; 4 5// Mock the entire ExchangeRateService using Jest 6jest.mock('../src/exchange-rate-service'); 7 8describe('PricingService', () => { 9 let pricingService: PricingService; 10 let mockExchangeRateService: jest.Mocked<ExchangeRateService>; 11 12 beforeEach(() => { 13 // Arrange 14 jest.clearAllMocks(); 15 mockExchangeRateService = new ExchangeRateService() as jest.Mocked<ExchangeRateService>; 16 pricingService = new PricingService(mockExchangeRateService); 17 }); 18 19 it('should convert prices using exchange rate', async () => { 20 // Arrange 21 mockExchangeRateService.getRate = jest.fn().mockResolvedValue(1.5); 22 23 // Act 24 const result = await pricingService.convertPrice(100, 'USD', 'EUR'); 25 26 // Assert 27 expect(mockExchangeRateService.getRate).toHaveBeenCalledWith('USD', 'EUR'); 28 expect(result).toBe(150.00); 29 }); 30});

In this setup:

  • We use jest.mock() to replace the ExchangeRateService with a mock version.
  • mockExchangeRateService.getRate is transformed into a function that we can program to return controlled values using jest.fn().mockResolvedValue(1.5).

When using jest.mock('../src/exchange-rate-service'), Jest replaces all functions in ExchangeRateService with mocked versions. These versions do not contain any real logic but allow control over returned values and behavior. By typing mockExchangeRateService as jest.Mocked<ExchangeRateService>, we ensure Jest recognizes it as a mock with access to Jest’s mock methods. This typing is essential because it clarifies to TypeScript that our dependency has been mocked and includes methods like mockResolvedValue.

Hands-On Example: PricingService with Mocks

Next, we write the necessary code in the PricingService to pass the test:

TypeScript
1export class PricingService { 2 constructor(private exchangeRateService: ExchangeRateService) {} 3 4 async convertPrice(amount: number, fromCurrency: string, toCurrency: string): Promise<number> { 5 const rate = await this.exchangeRateService.getRate(fromCurrency, toCurrency); 6 return Number((amount * rate).toFixed(2)); 7 } 8}

Now, when we run the test, it should pass, as we've coded just enough logic to meet the test's expectations.

Advanced Mocking Techniques

Mocks aren't limited to simple returns; they can simulate complex interactions, handle exceptions, or even return different values based on input:

TypeScript
1mockExchangeRateService.getRate = jest.fn() 2 .mockImplementation(async (from, to) => { 3 if (from === 'USD' && to === 'EUR') return 0.85; 4 if (from === 'USD' && to === 'GBP') return 0.73; 5 return 1.0; 6 });

This flexibility allows for comprehensive testing of edge cases and alternative paths, ensuring your application handles real-world scenarios robustly.

Summary and Practice Preparation

In this lesson, we explored the power of mocks within the TDD framework. By isolating dependencies using mocks, we can ensure our tests are reliable and focused on our application's logic:

  • Red: Begin with a failing test to clarify the functionality you're developing.
  • Green: Implement the minimum necessary code using mocks to pass the test.
  • Refactor: Clean and refine code without changing its behavior.

You're now ready to enter the practice exercises, where you'll apply these concepts to further solidify your understanding. By mastering these skills, you're well on your way to creating more robust and scalable applications. Keep practicing, and congratulations on reaching this point in the course!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.