#rails#rspec#rack#webmock

Mock External Services in Rails with WebMock and Rack

Sujay Prabhu's avatar

Sujay Prabhu

When developing applications in Rails, it is common to interact with external APIs such as payment gateway or notification services. Testing these applications should prevent actual HTTP calls to external services. Relying on actual HTTP requests can introduce issues such as server downtime or network erros which may end up in flaky tests and performance issues. Instead we can mock external service responses in the test environment using Webmock.

What is Webmock?

Webmock is a library for stubbing and setting expectations on HTTP requests in Ruby. It ensures that no external HTTP requests are made in test environment unless it is allowed explicitly.

How to stub requests with Webmock?

Consider a scenario where an endpoint in the Rails app fetches a set of todos from the JSONPlaceholder API

def index
  @client = JsonPlaceholder::Client.new
  @todos = @client.get_todos
end

Here's how you can test this endpoint using WebMock and RSpec

describe "GET /todos" do
  it "returns a list of todos with correct attributes" do
    stub_request(:get, "https://jsonplaceholder.typicode.com/todos")
      .to_return(status: 200, body: {
        todos: [
          {
            id: 1,
            title: "delectus aut autem",
            completed: false
          }
        ]
      }.to_json, headers: {})

    get todos_path

    expect(response).to have_http_status(200)

    first_todo = response.parsed_body["todos"].first
    expect(first_todo["id"]).to eq(1)
    expect(first_todo["title"]).to eq("delectus aut autem")
    expect(first_todo["completed"]).to eq(false)
  end
end

What is Rack?

A Rack application is a simple interface that responds to #call and takes a hash as an argument, returning an array of status, headers and a body. The body needs to respond to #each and then return strings that represent the response body.

Example of a basic Rack application:

class HelloWorld
  def call(env)
    [200, {"Content-Type" => "text/plain"}, ["Hello world!"]]
  end
end

How to use Rack with Webmock?

  1. Create a Rack compatible class

Implement a class that acts as a Rack application and returns responses based on request details. Here's an example that stubs the /todos endpoint

# spec/webmock/wemock_jsonplaceholder.rb

class WebmockJsonplaceholder
  # This class defines a call method that checks the HTTP request method (GET) and path (/todos).
  # If it matches, a 200 response with the content of todos_success_response.json is returned.
  # Otherwise, a 404 response is returned.

  SPEC_FIXTURES_PATH = 'spec/fixtures'.freeze
  TODOS_SUCCESS_RESPONSE = File.read("#{SPEC_FIXTURES_PATH}/todos_success_response.json").freeze

  def self.call(env)
    new.call(env)
  end

  def call(env)
    action = env['REQUEST_METHOD']
    path = env['PATH_INFO']
    
    if action == 'GET' && path == '/todos'
      [200, {}, [TODOS_SUCCESS_RESPONSE]]
    else
      [404, {}, ['NOT FOUND']]
    end
  end
end
  1. Integrate the stub in Rails Tests

In rails_helper.rb, add the following configuration to stub all requests to jsonplaceholder.typicode.com with the custom Rack app

require 'webmock/webmock_jsonplaceholder'

config.before(:each) do
  stub_request(:any, /jsonplaceholder.typicode.com/).to_rack(WebmockJsonplaceholder)
end
  1. Write Your Test

Now, you can write your RSpec test as if the request is hitting the actual external service

it "returns a list of todos with correct attributes" do
  get todos_path
  expect(response).to have_http_status(200)

  first_todo = response.parsed_body["todos"].first
  expect(first_todo["id"]).to eq(1)
  expect(first_todo["title"]).to eq("delectus aut autem")
  expect(first_todo["completed"]).to eq(false)
end

Why Combine WebMock with Rack?

Using WebMock with Rack enables you to create a mock server-like behavior that dynamically processes HTTP requests and returns flexible responses. While stub_request is great for simple, static responses, combining it with Rack allows for more complex and dynamic testing scenarios, such as:

  1. Simulating conditional responses based on request parameters or headers.
  2. Mimicking real-world server behavior that may involve custom status codes, headers, or logic.
  3. Reusability, allowing you to use the same Rack-based logic in multiple tests for consistency.

This approach enhances your tests by making them more robust, and reflects actual interactions with external services.

Related artcies