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?
- 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
- 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
- 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:
- Simulating conditional responses based on request parameters or headers.
- Mimicking real-world server behavior that may involve custom status codes, headers, or logic.
- 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.