Table of contents

How to Mock HTTParty Requests for Testing Purposes

When building Ruby applications that consume external APIs using HTTParty, it's essential to mock HTTP requests during testing to ensure your tests are fast, reliable, and don't depend on external services. This comprehensive guide covers various approaches to mocking HTTParty requests, from simple stubs to sophisticated recording and playback systems.

Why Mock HTTParty Requests?

Mocking HTTP requests in tests provides several critical benefits:

  • Speed: Eliminates network latency and external API response times
  • Reliability: Tests don't fail due to network issues or external service downtime
  • Predictability: Control exact responses and test edge cases
  • Cost efficiency: Avoid charges from paid APIs during testing
  • Isolation: Test your code logic independently of external dependencies

Setting Up WebMock for HTTParty Testing

WebMock is the most popular gem for stubbing HTTP requests in Ruby applications. It integrates seamlessly with HTTParty and provides a clean API for mocking requests.

Installation and Configuration

Add WebMock to your Gemfile's test group:

# Gemfile
group :test do
  gem 'webmock'
  gem 'rspec'
end

Configure WebMock in your test setup:

# spec/spec_helper.rb
require 'webmock/rspec'

RSpec.configure do |config|
  # Disable real HTTP requests during tests
  config.before(:suite) do
    WebMock.disable_net_connect!(allow_localhost: true)
  end
end

Basic HTTParty Request Mocking

Let's start with a simple example of an HTTParty service class and how to mock its requests:

# app/services/api_client.rb
class ApiClient
  include HTTParty
  base_uri 'https://api.example.com'

  def get_user(id)
    self.class.get("/users/#{id}")
  end

  def create_user(user_data)
    self.class.post('/users', {
      body: user_data.to_json,
      headers: { 'Content-Type' => 'application/json' }
    })
  end
end

Mocking GET Requests

Here's how to mock a simple GET request using WebMock:

# spec/services/api_client_spec.rb
require 'rails_helper'

RSpec.describe ApiClient do
  let(:client) { ApiClient.new }

  describe '#get_user' do
    it 'returns user data from the API' do
      user_response = {
        id: 123,
        name: 'John Doe',
        email: 'john@example.com'
      }

      # Stub the HTTP request
      stub_request(:get, 'https://api.example.com/users/123')
        .to_return(
          status: 200,
          body: user_response.to_json,
          headers: { 'Content-Type' => 'application/json' }
        )

      response = client.get_user(123)

      expect(response.code).to eq(200)
      expect(response.parsed_response['name']).to eq('John Doe')
    end
  end
end

Mocking POST Requests with Request Body Validation

For POST requests, you can also validate the request body:

describe '#create_user' do
  it 'creates a new user via API' do
    user_data = { name: 'Jane Doe', email: 'jane@example.com' }

    stub_request(:post, 'https://api.example.com/users')
      .with(
        body: user_data.to_json,
        headers: { 'Content-Type' => 'application/json' }
      )
      .to_return(
        status: 201,
        body: { id: 456, **user_data }.to_json,
        headers: { 'Content-Type' => 'application/json' }
      )

    response = client.create_user(user_data)

    expect(response.code).to eq(201)
    expect(response.parsed_response['id']).to eq(456)
  end
end

Advanced Mocking Techniques

Dynamic Response Generation

You can create dynamic responses based on request parameters:

stub_request(:get, /https:\/\/api\.example\.com\/users\/\d+/)
  .to_return do |request|
    user_id = request.uri.path.split('/').last
    {
      status: 200,
      body: { id: user_id.to_i, name: "User #{user_id}" }.to_json,
      headers: { 'Content-Type' => 'application/json' }
    }
  end

Simulating Network Errors

Test how your application handles various failure scenarios:

describe 'error handling' do
  it 'handles timeout errors gracefully' do
    stub_request(:get, 'https://api.example.com/users/123')
      .to_timeout

    expect { client.get_user(123) }.to raise_error(Net::TimeoutError)
  end

  it 'handles server errors' do
    stub_request(:get, 'https://api.example.com/users/123')
      .to_return(status: 500, body: 'Internal Server Error')

    response = client.get_user(123)
    expect(response.code).to eq(500)
  end
end

Request Verification

Verify that requests were made with correct parameters:

it 'makes the correct API call' do
  stub_request(:get, 'https://api.example.com/users/123')
    .to_return(status: 200, body: '{}')

  client.get_user(123)

  expect(WebMock).to have_requested(:get, 'https://api.example.com/users/123')
    .with(headers: { 'User-Agent' => /HTTParty/ })
end

Using VCR for Record and Playback Testing

VCR (Video Cassette Recorder) is another powerful tool that records real HTTP interactions and replays them during tests. This approach is particularly useful when you want to use real API responses but still maintain test isolation.

VCR Setup

Add VCR to your Gemfile:

# Gemfile
group :test do
  gem 'vcr'
  gem 'webmock'
end

Configure VCR:

# spec/spec_helper.rb
require 'vcr'

VCR.configure do |config|
  config.cassette_library_dir = 'spec/fixtures/vcr_cassettes'
  config.hook_into :webmock
  config.configure_rspec_metadata!
  config.allow_http_connections_when_no_cassette = false
end

Using VCR in Tests

describe '#get_user', :vcr do
  it 'returns user data from the API' do
    response = client.get_user(123)

    expect(response.code).to eq(200)
    expect(response.parsed_response).to have_key('name')
  end
end

The first time this test runs, VCR will record the actual HTTP request. Subsequent runs will use the recorded response, making tests fast and deterministic.

Testing HTTParty with RSpec Shared Examples

Create reusable test patterns for common HTTParty scenarios:

# spec/support/shared_examples/api_client.rb
RSpec.shared_examples 'an API client' do |endpoint, method|
  it 'handles successful responses' do
    stub_request(method, endpoint)
      .to_return(status: 200, body: '{"success": true}')

    response = subject.public_send(method.to_s.downcase, endpoint)
    expect(response.code).to eq(200)
  end

  it 'handles error responses' do
    stub_request(method, endpoint)
      .to_return(status: 404, body: '{"error": "Not found"}')

    response = subject.public_send(method.to_s.downcase, endpoint)
    expect(response.code).to eq(404)
  end
end

Use the shared examples in your tests:

describe ApiClient do
  subject { described_class.new }

  it_behaves_like 'an API client', 'https://api.example.com/users', :get
end

Mocking Authentication and Headers

When working with APIs that require authentication, mock the authentication flow. For instance, when testing web scraping scenarios that require handling authentication in web scraping applications, you'll want to ensure your HTTP client properly manages session tokens:

class AuthenticatedApiClient
  include HTTParty
  base_uri 'https://api.example.com'

  def initialize(api_key)
    @api_key = api_key
  end

  def get_protected_resource
    self.class.get('/protected', headers: auth_headers)
  end

  private

  def auth_headers
    { 'Authorization' => "Bearer #{@api_key}" }
  end
end

Test with authentication headers:

describe AuthenticatedApiClient do
  let(:client) { AuthenticatedApiClient.new('test-api-key') }

  it 'includes authentication headers' do
    stub_request(:get, 'https://api.example.com/protected')
      .with(headers: { 'Authorization' => 'Bearer test-api-key' })
      .to_return(status: 200, body: '{"data": "secret"}')

    response = client.get_protected_resource
    expect(response.code).to eq(200)
  end
end

Best Practices for HTTParty Testing

1. Use Factory Methods for Consistent Test Data

# spec/support/factories/api_responses.rb
module ApiResponseFactories
  def user_response(id: 123, name: 'Test User')
    {
      id: id,
      name: name,
      email: "#{name.downcase.gsub(' ', '.')}@example.com",
      created_at: Time.current.iso8601
    }
  end

  def error_response(message: 'Something went wrong')
    { error: message, timestamp: Time.current.iso8601 }
  end
end

RSpec.configure do |config|
  config.include ApiResponseFactories
end

2. Test Both Success and Failure Scenarios

Always test how your application handles various response codes and error conditions. This is especially important when implementing retry logic for failed API requests:

describe 'error handling' do
  context 'when API returns 401 Unauthorized' do
    it 'raises an authentication error' do
      stub_request(:get, 'https://api.example.com/users/123')
        .to_return(status: 401, body: error_response('Invalid token').to_json)

      expect { client.get_user(123) }.to raise_error(AuthenticationError)
    end
  end

  context 'when API returns 429 Rate Limited' do
    it 'implements retry logic' do
      stub_request(:get, 'https://api.example.com/users/123')
        .to_return(status: 429)
        .then
        .to_return(status: 200, body: user_response.to_json)

      response = client.get_user_with_retry(123)
      expect(response.code).to eq(200)
    end
  end
end

3. Organize Tests with Contexts

Use RSpec contexts to group related test scenarios:

describe ApiClient do
  describe '#get_user' do
    context 'when user exists' do
      before do
        stub_request(:get, 'https://api.example.com/users/123')
          .to_return(status: 200, body: user_response.to_json)
      end

      it 'returns the user data' do
        # test implementation
      end
    end

    context 'when user does not exist' do
      before do
        stub_request(:get, 'https://api.example.com/users/999')
          .to_return(status: 404, body: error_response('User not found').to_json)
      end

      it 'returns 404 status' do
        # test implementation
      end
    end
  end
end

4. Mock Concurrent Requests

When testing applications that make concurrent requests for faster scraping, ensure your mocks handle multiple simultaneous requests:

describe 'concurrent requests' do
  it 'handles multiple requests simultaneously' do
    # Mock multiple user endpoints
    (1..5).each do |id|
      stub_request(:get, "https://api.example.com/users/#{id}")
        .to_return(status: 200, body: user_response(id: id).to_json)
    end

    # Test concurrent fetching
    responses = (1..5).map { |id| Thread.new { client.get_user(id) } }
                     .map(&:value)

    expect(responses).to all(have_attributes(code: 200))
  end
end

Integration with CI/CD

Ensure your mocked tests run consistently in continuous integration environments:

# .github/workflows/test.yml
name: Test
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true
      - name: Run tests
        run: bundle exec rspec
        env:
          RAILS_ENV: test

Testing Rate Limiting and Retry Logic

When implementing rate limiting protection, test both the detection and response mechanisms:

describe 'rate limiting' do
  it 'respects rate limits' do
    # First request succeeds
    stub_request(:get, 'https://api.example.com/users/1')
      .to_return(status: 200, body: user_response.to_json)

    # Second request hits rate limit
    stub_request(:get, 'https://api.example.com/users/2')
      .to_return(
        status: 429,
        headers: { 'Retry-After' => '60' },
        body: { error: 'Rate limit exceeded' }.to_json
      )

    first_response = client.get_user(1)
    expect(first_response.code).to eq(200)

    second_response = client.get_user(2)
    expect(second_response.code).to eq(429)
    expect(second_response.headers['retry-after']).to eq('60')
  end
end

Conclusion

Mocking HTTParty requests is essential for creating reliable, fast, and maintainable test suites. Whether you choose WebMock for fine-grained control or VCR for recording real interactions, the key is to test both successful scenarios and error conditions comprehensively.

By implementing proper mocking strategies, you ensure that your application gracefully handles various API responses and network conditions. This approach leads to more robust applications and gives you confidence when deploying changes that interact with external services.

Remember to keep your mocks realistic and update them when the external API changes. Regular integration tests against the real API (perhaps in a staging environment) can help ensure your mocks remain accurate and your application continues to work correctly with the actual service.

Try WebScraping.AI for Your Web Scraping Needs

Looking for a powerful web scraping solution? WebScraping.AI provides an LLM-powered API that combines Chromium JavaScript rendering with rotating proxies for reliable data extraction.

Key Features:

  • AI-powered extraction: Ask questions about web pages or extract structured data fields
  • JavaScript rendering: Full Chromium browser support for dynamic content
  • Rotating proxies: Datacenter and residential proxies from multiple countries
  • Easy integration: Simple REST API with SDKs for Python, Ruby, PHP, and more
  • Reliable & scalable: Built for developers who need consistent results

Getting Started:

Get page content with AI analysis:

curl "https://api.webscraping.ai/ai/question?url=https://example.com&question=What is the main topic?&api_key=YOUR_API_KEY"

Extract structured data:

curl "https://api.webscraping.ai/ai/fields?url=https://example.com&fields[title]=Page title&fields[price]=Product price&api_key=YOUR_API_KEY"

Try in request builder

Related Questions

Get Started Now

WebScraping.AI provides rotating proxies, Chromium rendering and built-in HTML parser for web scraping
Icon