Back to Repositories

Testing HTTP Client Implementation with Faraday Stubs in lostisland/faraday

This test suite demonstrates comprehensive testing of a Faraday HTTP client implementation, focusing on request handling, parameter encoding, and response processing. The tests verify both successful and error scenarios using RSpec’s testing framework with Faraday’s stub adapter.

Test Coverage Overview

The test suite provides extensive coverage of HTTP client functionality including:

  • Basic GET request handling and response parsing
  • Error handling for 404 responses
  • Connection failure scenarios
  • Parameter encoding with strict mode validation
  • Multiple URL parameter handling using FlatParamsEncoder
  • POST request body verification using both string and proc matchers

Implementation Analysis

The testing approach utilizes Faraday’s test adapter with stubs to simulate HTTP interactions without actual network calls. The implementation leverages RSpec’s context blocks and let statements to organize test scenarios and maintain clean test setup. Key patterns include response stubbing, expectation verification, and custom matcher usage for request validation.

Technical Details

  • Testing Framework: RSpec
  • HTTP Client: Faraday
  • Test Doubles: Faraday::Adapter::Test::Stubs
  • Request Encoding: Faraday::FlatParamsEncoder
  • Response Format: JSON
  • Error Handling: ConnectionFailed, 404 responses

Best Practices Demonstrated

The test suite exemplifies several testing best practices including isolated test cases, comprehensive error scenario coverage, and thorough request validation. It demonstrates proper stub verification, clean test organization using RSpec contexts, and effective use of helper methods and shared setup through let blocks. The tests also show proper handling of different parameter encoding scenarios and request body validation techniques.

lostisland/faraday

examples/client_spec.rb

            
# frozen_string_literal: true

# Requires Ruby with rspec and faraday gems.
# rspec client_spec.rb

require 'faraday'
require 'json'

# Example API client
class Client
  def initialize(conn)
    @conn = conn
  end

  def httpbingo(jname, params: {})
    res = @conn.get("/#{jname}", params)
    data = JSON.parse(res.body)
    data['origin']
  end

  def foo(params)
    res = @conn.post('/foo', JSON.dump(params))
    res.status
  end
end

RSpec.describe Client do
  let(:stubs)  { Faraday::Adapter::Test::Stubs.new }
  let(:conn)   { Faraday.new { |b| b.adapter(:test, stubs) } }
  let(:client) { Client.new(conn) }

  it 'parses origin' do
    stubs.get('/ip') do |env|
      # optional: you can inspect the Faraday::Env
      expect(env.url.path).to eq('/ip')
      [
        200,
        { 'Content-Type': 'application/javascript' },
        '{"origin": "127.0.0.1"}'
      ]
    end

    # uncomment to trigger stubs.verify_stubbed_calls failure
    # stubs.get('/unused') { [404, {}, ''] }

    expect(client.httpbingo('ip')).to eq('127.0.0.1')
    stubs.verify_stubbed_calls
  end

  it 'handles 404' do
    stubs.get('/api') do
      [
        404,
        { 'Content-Type': 'application/javascript' },
        '{}'
      ]
    end
    expect(client.httpbingo('api')).to be_nil
    stubs.verify_stubbed_calls
  end

  it 'handles exception' do
    stubs.get('/api') do
      raise Faraday::ConnectionFailed
    end

    expect { client.httpbingo('api') }.to raise_error(Faraday::ConnectionFailed)
    stubs.verify_stubbed_calls
  end

  context 'When the test stub is run in strict_mode' do
    let(:stubs) { Faraday::Adapter::Test::Stubs.new(strict_mode: true) }

    it 'verifies the all parameter values are identical' do
      stubs.get('/api?abc=123') do
        [
          200,
          { 'Content-Type': 'application/javascript' },
          '{"origin": "127.0.0.1"}'
        ]
      end

      # uncomment to raise Stubs::NotFound
      # expect(client.httpbingo('api', params: { abc: 123, foo: 'Kappa' })).to eq('127.0.0.1')
      expect(client.httpbingo('api', params: { abc: 123 })).to eq('127.0.0.1')
      stubs.verify_stubbed_calls
    end
  end

  context 'When the Faraday connection is configured with FlatParamsEncoder' do
    let(:conn) { Faraday.new(request: { params_encoder: Faraday::FlatParamsEncoder }) { |b| b.adapter(:test, stubs) } }

    it 'handles the same multiple URL parameters' do
      stubs.get('/api?a=x&a=y&a=z') { [200, { 'Content-Type' => 'application/json' }, '{"origin": "127.0.0.1"}'] }

      # uncomment to raise Stubs::NotFound
      # expect(client.httpbingo('api', params: { a: %w[x y] })).to eq('127.0.0.1')
      expect(client.httpbingo('api', params: { a: %w[x y z] })).to eq('127.0.0.1')
      stubs.verify_stubbed_calls
    end
  end

  context 'When you want to test the body, you can use a proc as well as string' do
    it 'tests with a string' do
      stubs.post('/foo', '{"name":"YK"}') { [200, {}, ''] }

      expect(client.foo(name: 'YK')).to eq 200
      stubs.verify_stubbed_calls
    end

    it 'tests with a proc' do
      check = ->(request_body) { JSON.parse(request_body).slice('name') == { 'name' => 'YK' } }
      stubs.post('/foo', check) { [200, {}, ''] }

      expect(client.foo(name: 'YK', created_at: Time.now)).to eq 200
      stubs.verify_stubbed_calls
    end
  end
end