June 6, 2025
Abstract
When testing systems that depend on external services, such as geolocation APIs, simulating HTTP responses becomes essential for building deterministic and isolated test suites. This article explores a disciplined approach to mocking HTTP calls in RSpec using WebMock—without relying on custom helpers or implicit abstractions. Instead, we emphasize clean code principles: clarity, locality, and test independence.
Let’s Talk Clean Code & Testing
If this article sparked your interest in writing expressive, reliable specs using WebMock, I’d love to connect. Whether you’re into code reviews, Ruby testing patterns, or API architecture, drop me a message!
Introduction
In modern web applications, integration with third-party APIs is unavoidable. While these dependencies provide valuable services (e.g., geocoding, payment processing), they pose a challenge for testability: tests must not depend on the availability or behavior of external systems.
Tools like WebMock allow developers to stub HTTP requests and simulate API responses, enabling unit and integration tests to run reliably and quickly. Yet, the way we integrate WebMock into our test suite matters. Overuse of abstraction or indirection can make tests hard to follow or maintain.
This article presents a case study of using WebMock with RSpec to simulate HTTP responses clearly and effectively, following clean code principles without resorting to helper methods or external dependencies like VCR.
Core Concepts
Isolation: Tests should not reach out to external services. They must fail for changes in application logic, not network availability.
Determinism: A test should produce the same result each time it runs.
Clarity: Tests are not just for machines. They should act as documentation for future developers (and your future self).
Minimal Abstraction: Avoid extracting logic into helpers unless repeated across many examples. Indirection harms readability when overused.
Implementation
Let’s walk through a real-world scenario using a geocoding service.
Example: Geocoding in a Model
We want to test a method that fetches coordinates for a given location using the OpenCage Data API. Below is a self-contained example of how to stub those responses cleanly.
# spec/models/location_spec.rb
require "webmock/rspec"
RSpec.describe Location, type: :model do
before(:each) { Location.delete_all }
describe ".find_or_create_with_coordinates" do
it "creates a location using latitude and longitude from the geocoding API" do
lat = 41.38879
lng = 2.15899
address = "Barcelona, Catalonia, Spain"
query = "Barcelona,Spain"
stub_request(:get, "http://api.opencagedata.com/geocode/v1/json")
.with(query: hash_including({ q: query }))
.to_return(
status: 200,
headers: { "Content-Type" => "application/json" },
body: {
status: { code: 200 },
results: [
{
geometry: { lat: lat, lng: lng },
formatted_address: address
}
]
}.to_json
)
location = Location.find_or_create_with_coordinates(location_name: "Barcelona", country: "Spain")
expect(location).to be_persisted
expect(location.latitude).to eq(lat)
expect(location.longitude).to eq(lng)
end
end
end
Using Fixtures for Repeatability
Instead of writing JSON directly in each test, we can store the full response as a fixture for reuse and clarity. This maintains the test’s determinism and expressiveness without abstraction via helpers.
Example:
# spec/fixtures/opencage/barcelona.json
{
"status": { "code": 200, "message": "OK" },
"results": [
{
"geometry": { "lat": 41.38879, "lng": 2.15899 },
"formatted_address": "Barcelona, Catalonia, Spain"
}
]
}
In the spec:
body = File.read(Rails.root.join("spec/fixtures/opencage/barcelona.json"))
stub_request(:get, /opencagedata.com/)
.to_return(status: 200, body: body, headers: { "Content-Type" => "application/json" })
Design Choices and Justification
- Avoiding Helpers: While helpers are useful when stubbing is repeated across multiple examples, they can obscure the cause-effect relationship between test setup and assertion. We opt to keep stubs close to the logic they support.
- Explicit Stubs: Each test shows exactly what external data the system is consuming and how it behaves. This aids in understanding both business logic and API assumptions.
- Optional Fixtures: For larger payloads, fixtures allow storing sample JSON responses while keeping the spec clean and readable.
- Clear Variable Naming: Variables like lat, lng, and query make expectations easy to follow, both for developers and reviewers.
Conclusion
Mocking external APIs in tests is not just about eliminating network calls—it’s an opportunity to write expressive, robust documentation for how your code interacts with third-party systems. By keeping stubs explicit and close to the tests they support, we increase maintainability without sacrificing readability.
This approach embraces simplicity and transparency over abstraction, resulting in tests that are both isolated and communicative.
References
- WebMock Documentation: https://github.com/bblimke/webmock
- RSpec Best Practices: https://rspec.info
- OpenCage Geocoder API: https://opencagedata.com
Top comments (0)