Lesson 4
Introduction to Mocks with Ruby and RSpec
Introduction to Mocks

In our journey through isolating dependencies with test doubles, we've explored dummies, stubs, and spies. This lesson focuses on mocks, a powerful type of test double used in Ruby to simulate external dependencies in software tests. You may have noticed in the previous unit how spying on the real implementation can become cumbersome. Mocks can imitate the behavior of complex systems, allowing us to test code in isolation without relying on real, 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 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 example, when writing tests for a PricingService, you don't want the tests to fail due to external issues like a currency conversion API being down. Mocks provide a controlled environment where you can simulate various conditions and responses as well as validate the calls.

Mocks differ from spies in that they fully simulate dependencies rather than just observing behavior. A mock creates a controlled substitute for a dependency, ensuring that 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 RSpec

Let's dive into mocking with RSpec. We'll start with the basics: how to mock a class and its methods in Ruby.

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 RSpec:

Ruby
1# In spec/pricing_service_spec.rb 2RSpec.describe PricingService do 3 # Create a mock double for the ExchangeRateService 4 let(:mock_exchange_rate_service) { double("ExchangeRateService") } 5 6 # Instantiate PricingService with the mock ExchangeRateService 7 let(:pricing_service) { PricingService.new(mock_exchange_rate_service) } 8 9 it 'converts prices using exchange rate' do 10 # Arrange: Set up the mock to return a specific value when get_rate is called 11 allow(mock_exchange_rate_service).to receive(:get_rate).with('USD', 'EUR').and_return(1.5) 12 13 # Act: Call the method under test 14 result = pricing_service.convert_price(100.0, "USD", "EUR") 15 16 # Assert: Check that get_rate was called once and verify the result 17 expect(mock_exchange_rate_service).to have_received(:get_rate).with('USD', 'EUR').once 18 expect(result).to eq(150.00) 19 end 20end

In this setup:

  • We use double to create a mock version of the ExchangeRateService.
  • allow(mock_exchange_rate_service).to receive is used to control the return value of the method we're simulating.

This approach ensures that tests are decoupled from external systems.

Hands-On Example: PricingService with Mocks

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

Ruby
1class PricingService 2 # Initialize with an instance of ExchangeRateService or its mock 3 def initialize(exchange_rate_service) 4 @exchange_rate_service = exchange_rate_service 5 end 6 7 # Convert the price between currencies using the exchange rate service 8 def convert_price(amount, from_currency, to_currency) 9 rate = @exchange_rate_service.get_rate(from_currency, to_currency) # Get the conversion rate 10 amount * rate # Calculate and return the converted amount 11 end 12end

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 can simulate complex interactions, handle exceptions, or return different values based on input using and_return and blocks:

Ruby
1# Allow the mock to return different rates based on the to_currency 2allow(mock_exchange_rate_service).to receive(:get_rate) do |from_currency, to_currency| 3 case to_currency 4 when 'EUR' then 0.85 # Return 0.85 for EUR 5 when 'GBP' then 0.73 # Return 0.73 for GBP 6 end 7end

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.