Lesson 2
Using Stubs in Test Driven Development with Ruby
Introduction and Context Setting

Welcome to the second lesson in our exploration of Test Driven Development (TDD) with Ruby and RSpec. 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 double: 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 about 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 WeatherAlertService that will get its data from a WeatherService, which receives weather data and issues 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 WeatherService, we can define what the WeatherData looks like.

The class is structured as follows:

Ruby
1class WeatherData 2 # Defines a class to represent weather data with temperature and conditions 3 attr_reader :temperature, :conditions 4 5 def initialize(temperature, conditions) 6 # Initializes the WeatherData object with given temperature and conditions 7 @temperature = temperature 8 @conditions = conditions 9 end 10end
Red: Writing the First Test

In a test file named weather_alert_service_spec.rb, you can include the following test cases:

Ruby
1require 'rspec' 2require_relative 'weather_alert_service' 3 4class WeatherServiceStub 5 # A stub class to simulate a weather service's behavior in tests 6 def initialize 7 @temperature = 20 8 @conditions = "sunny" 9 end 10 11 def set_weather(temperature, conditions) 12 # Allows tests to set predefined weather data 13 @temperature = temperature 14 @conditions = conditions 15 end 16 17 def get_current_weather(_location) 18 # Returns predefined weather data for any location 19 WeatherData.new(@temperature, @conditions) 20 end 21end 22 23RSpec.describe WeatherAlertService do 24 let(:weather_service_stub) { WeatherServiceStub.new } 25 let(:alert_service) { WeatherAlertService.new(weather_service_stub) } 26 27 it 'should return heat warning when temperature is above 35' do 28 # Sets the test scenario to a high temperature and validates alert message 29 weather_service_stub.set_weather(36, "sunny") 30 31 # Calls the method we are testing 32 alert = alert_service.should_send_alert("London") 33 34 # Checks if the method returns the expected alert 35 expect(alert).to eq("Extreme heat warning. Stay hydrated!") 36 end 37end

Here:

  • We implement a stub, WeatherServiceStub, to simulate weather data. We add the set_weather method, which allows tests to pre-define the results from the service.
  • We write our first RSpec test to check if WeatherAlertService processes weather data correctly.

If we run this test, we expect it to fail initially as WeatherAlertService is not yet implemented to handle the specified conditions.

Green: Making the Test Pass

In a file named weather_alert_service.rb, you can implement the should_send_alert method with minimal logic:

Ruby
1class WeatherAlertService 2 # A service that determines if a weather alert should be sent 3 def initialize(weather_service) 4 # Initializes the service with a weather service dependency 5 @weather_service = weather_service 6 end 7 8 def should_send_alert(location) 9 # Determines if an alert should be sent based on current weather 10 weather = @weather_service.get_current_weather(location) 11 12 # Returns a heat warning if temperature exceeds threshold 13 if weather.temperature > 35 14 "Extreme heat warning. Stay hydrated!" 15 else 16 nil 17 end 18 end 19end

If we run the tests again, our goal is to pass the specific test scenario by implementing only the needed logic without covering more cases yet.

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 help 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 enhancing your understanding and skill set in crafting well-tested, robust Ruby code.

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