Lesson 2
Isolating Dependencies with Test Doubles: Stubs
Introduction and Context Setting

Welcome to the second lesson in our deep dive into Test Driven Development (TDD) with TypeScript and Jest. Previously, you learned about using dummies to isolate dependencies. In this lesson, we'll focus on another type of test double — Stubs.

By the end of this lesson, you'll understand what stubs are and how to implement them in your tests, specifically for isolating dependencies like external services.

Understanding Stubs in Testing

Test doubles help us isolate parts of our application for testing. We've previously discussed dummies; now, let's move to a slightly more useful Test Doubple: stubs. Stubs are predefined answers to method calls made during testing. They don't track how they're used, unlike more complex Test Doubles that we'll learn later.

Stubs are beneficial when you're testing a function that relies on an external service or complex dependency. They let you simulate the function's output, making your tests faster and more predictable. Keep in mind, though, that stubs primarily ensure your application logic functions as intended and don't necessarily verify the correctness of external dependencies.

Stubs are used to provide consistent, predefined responses to specific method calls in tests, allowing you to control the behavior of certain dependencies without implementing their actual functionality. Unlike dummies, which are mere placeholders, stubs actively simulate responses, making them useful when you need predictable outcomes from dependencies. This predictability allows you to isolate and test your application’s logic without needing to rely on the behavior of external systems, which is especially helpful when those systems may be complex or introduce variability.

Example: Crafting a Weather Alert Service using Stubs

Let’s apply the TDD workflow to build a WeatherAlertService using stubs.

Getting ready to test

We are building a Wether Alert Service which will get its data from a WeatherAlertService that receives weather data and issue alerts based on certain conditions. The WeatherAlertService will need to fetch data from an external data source which is not feasible in a testing environment. For the tests that rely on data from the WeatherAlertService, we'll replace it with Stubbed-out data insteaed.

The interface looks like this:

TypeScript
1export interface WeatherData { 2 temperature: number; 3 conditions: string; 4} 5 6export interface WeatherService { 7 getCurrentWeather(location: string): Promise<WeatherData>; 8}
Red: Writing the First Test

Create a test file named weather-alert-service.test.ts in your test directory with the following test cases:

TypeScript
1import { WeatherAlertService } from '../src/weather-alert-service'; 2import type { WeatherService, WeatherData } from '../src/types'; 3 4// Create the Stub for the WeatherService interface 5class WeatherServiceStub implements WeatherService { 6 private temperature: number = 20; 7 private conditions: string = "sunny"; 8 9 setWeather(temperature: number, conditions: string) { 10 this.temperature = temperature; 11 this.conditions = conditions; 12 } 13 14 async getCurrentWeather(_location: string): Promise<WeatherData> { 15 return { 16 temperature: this.temperature, 17 conditions: this.conditions 18 }; 19 } 20} 21 22describe('WeatherAlertService', () => { 23 let weatherService: WeatherServiceStub; 24 let alertService: WeatherAlertService; 25 26 beforeEach(() => { 27 weatherService = new WeatherServiceStub(); 28 alertService = new WeatherAlertService(weatherService); 29 }); 30 31 it('should return heat warning when temperature is above 35', async () => { 32 // Arrange 33 weatherService.setWeather(36, "sunny"); 34 35 // Act 36 const alert = await alertService.shouldSendAlert("London"); 37 38 // Assert 39 expect(alert).toBe("Extreme heat warning. Stay hydrated!"); 40 }); 41 42 // Additional tests follow... 43});

Here:

  • We implement a stub, WeatherServiceStub, to simulate weather data. We add the setWeather method, which is not part of the interface, so that the tests can pre-define the results from the service.
  • We write our first Jest test to check if WeatherAlertService processes weather data correctly.

Run this test, and expect it to fail initially as WeatherAlertService is not yet implemented to handle the specified conditions.

Green: Making the Test Pass

Implement the shouldSendAlert function with minimal logic:

Create weather-alert-service.ts in your src directory:

TypeScript
1import type { WeatherService } from "./types"; 2 3export class WeatherAlertService { 4 constructor(private weatherService: WeatherService) {} 5 6 async shouldSendAlert(location: string): Promise<string | null> { 7 const weather = await this.weatherService.getCurrentWeather(location); 8 9 if (weather.temperature > 35) { 10 return "Extreme heat warning. Stay hydrated!"; 11 } 12 13 return null; 14 } 15}

Run the tests again. The goal here is to pass the specific test scenario by implementing only the needed logic without covering more cases yet.

Output:

1PASS test/weather-alert-service.test.ts 2 WeatherAlertService 3 ✓ should return heat warning when temperature is above 35 (4 ms)
Refactor: Improving Code Quality

Although this test now passes, inspect and clean your code to enhance readability and maintainability:

  • Ensure variable names are clear and meaningful.
  • Optimize any redundant logic without altering the functionality.

There might not be significant refactoring needed here yet, but ensure you familiarize yourself with constantly enhancing your code while system tests stay green.

Summary and Preparation for Practice

In this lesson, we focused on integrating stubs as a practical solution for isolating dependencies in your tests. Here's a quick recap of what we covered:

  • Stubs provide predefined outputs, which helps in testing functionalities that depend on external services.
  • We worked through the Red-Green-Refactor cycle, emphasizing writing failing tests before developing logic and refining the implementation without changing its behavior.

Prepare to practice these concepts in the upcoming exercises, where you'll tackle various scenarios and get more comfortable with using stubs and the TDD approach. Keep experimenting with test doubles and enhance your understanding and skill set in crafting well-tested, robust code.

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