Back to Repositories

Testing HTTParty Response Handling in jnunemaker/httparty

This test suite comprehensively validates the HTTParty::Response class functionality, covering response handling, header management, and HTTP status code behaviors. The tests ensure proper initialization, method delegation, and semantic response code interpretation across various HTTP scenarios.

Test Coverage Overview

The test suite provides extensive coverage of HTTParty::Response functionality including:
  • Response object initialization and attribute setting
  • Header manipulation and access patterns
  • HTTP status code semantic methods
  • Error handling and status code validation
  • Response body parsing and delegation

Implementation Analysis

The testing approach utilizes RSpec’s behavior-driven development paradigm with detailed context organization.
  • Extensive use of before blocks for test setup
  • Mock objects for HTTP responses
  • Shared examples for common behaviors
  • Edge case handling for various HTTP scenarios

Technical Details

Testing infrastructure includes:
  • RSpec as the primary testing framework
  • Net::HTTP mock objects
  • Custom response headers implementation
  • Mock request objects
  • Lambda functions for parsed responses

Best Practices Demonstrated

The test suite exemplifies several testing best practices:
  • Comprehensive setup and teardown management
  • Isolated test cases with clear contexts
  • Thorough edge case coverage
  • Consistent test naming conventions
  • Proper use of RSpec expectations and matchers

jnunemaker/httparty

spec/httparty/response_spec.rb

            
require 'spec_helper'

RSpec.describe HTTParty::Response do
  before do
    @last_modified = Date.new(2010, 1, 15).to_s
    @content_length = '1024'
    @request_object = HTTParty::Request.new Net::HTTP::Get, '/'
    @response_object = Net::HTTPOK.new('1.1', 200, 'OK')
    allow(@response_object).to receive_messages(body: "{foo:'bar'}")
    @response_object['last-modified'] = @last_modified
    @response_object['content-length'] = @content_length
    @parsed_response = lambda { {"foo" => "bar"} }
    @response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
  end

  describe ".underscore" do
    it "works with one capitalized word" do
      expect(HTTParty::Response.underscore("Accepted")).to eq("accepted")
    end

    it "works with titlecase" do
      expect(HTTParty::Response.underscore("BadGateway")).to eq("bad_gateway")
    end

    it "works with all caps" do
      expect(HTTParty::Response.underscore("OK")).to eq("ok")
    end
  end

  describe "initialization" do
    it "should set the Net::HTTP Response" do
      expect(@response.response).to eq(@response_object)
    end

    it "should set body" do
      expect(@response.body).to eq(@response_object.body)
    end

    it "should set code" do
      expect(@response.code).to eq(@response_object.code)
    end

    it "should set code as an Integer" do
      expect(@response.code).to be_a(Integer)
    end

    it "should set http_version" do
      unparseable_body = lambda { raise "Unparseable" }
      unparseable_response = HTTParty::Response.new(@request_object, @response_object, unparseable_body)
      expect(unparseable_response.http_version).to eq(@response_object.http_version)
    end

    context 'test raise_on requests' do
      let(:raise_on) { [404] }
      let(:request) { HTTParty::Request.new(Net::HTTP::Get, '/', raise_on: raise_on) }
      let(:body)     { 'Not Found' }
      let(:response) { Net::HTTPNotFound.new('1.1', 404, body) }

      subject { described_class.new(request, response, @parsed_response) }

      before do
        allow(response).to receive(:body).and_return(body)
      end

      context 'when raise_on is a number' do
        let(:raise_on) { [404] }

        context "and response's status code is in range" do
          it 'throws exception' do
            expect{ subject }.to raise_error(HTTParty::ResponseError, "Code 404 - #{body}")
          end
        end

        context "and response's status code is not in range" do
          let(:response) { Net::HTTPNotFound.new('1.1', 200, body) }

          it 'does not throw exception' do
            expect{ subject }.not_to raise_error
          end
        end
      end

      context 'when raise_on is a regexpr' do
        let(:raise_on) { ['4[0-9]*'] }

        context "and response's status code is in range" do
          it 'throws exception' do
            expect{ subject }.to raise_error(HTTParty::ResponseError, "Code 404 - #{body}")
          end
        end

        context "and response's status code is not in range" do
          let(:response) { Net::HTTPNotFound.new('1.1', 200, body) }

          it 'does not throw exception' do
            expect{ subject }.not_to raise_error
          end
        end
      end
    end
  end

  it 'does raise an error about itself when using #method' do
    expect {
      HTTParty::Response.new(@request_object, @response_object, @parsed_response).method(:qux)
    }.to raise_error(NameError, /HTTParty\:\:Response/)
  end

  it 'does raise an error about itself when invoking a method that does not exist' do
    expect {
      HTTParty::Response.new(@request_object, @response_object, @parsed_response).qux
    }.to raise_error(NoMethodError, /HTTParty\:\:Response/)
  end

  it "returns response headers" do
    response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
    expect(response.headers).to eq({'last-modified' => [@last_modified], 'content-length' => [@content_length]})
  end

  it "should send missing methods to delegate" do
    response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
    expect(response['foo']).to eq('bar')
  end

  it "responds to request" do
    response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
    expect(response.respond_to?(:request)).to be_truthy
  end

  it "responds to response" do
    response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
    expect(response.respond_to?(:response)).to be_truthy
  end

  it "responds to body" do
    response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
    expect(response.respond_to?(:body)).to be_truthy
  end

  it "responds to headers" do
    response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
    expect(response.respond_to?(:headers)).to be_truthy
  end

  it "responds to parsed_response" do
    response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
    expect(response.respond_to?(:parsed_response)).to be_truthy
  end

  it "responds to predicates" do
    response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
    expect(response.respond_to?(:success?)).to be_truthy
  end

  it "responds to anything parsed_response responds to" do
    response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
    expect(response.respond_to?(:[])).to be_truthy
  end

  context 'response is array' do
    let(:response_value) { [{'foo' => 'bar'}, {'foo' => 'baz'}] }
    let(:response) { HTTParty::Response.new(@request_object, @response_object, lambda { response_value }) }
    it "should be able to iterate" do
      expect(response.size).to eq(2)
      expect {
        response.each { |item| }
      }.to_not raise_error
    end

    it 'should respond to array methods' do
      expect(response).to respond_to(:bsearch, :compact, :cycle, :delete, :each, :flatten, :flatten!, :compact, :join)
    end

    it 'should equal the string response object body' do
      expect(response.to_s).to eq(@response_object.body.to_s)
    end

    it 'should display the same as an array' do
      a = StringIO.new
      b = StringIO.new
      response_value.display(b)
      response.display(a)

      expect(a.string).to eq(b.string)
    end
  end

  it "allows headers to be accessed by mixed-case names in hash notation" do
    response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
    expect(response.headers['Content-LENGTH']).to eq(@content_length)
  end

  it "returns a comma-delimited value when multiple values exist" do
    @response_object.add_field 'set-cookie', 'csrf_id=12345; path=/'
    @response_object.add_field 'set-cookie', '_github_ses=A123CdE; path=/'
    response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
    expect(response.headers['set-cookie']).to eq("csrf_id=12345; path=/, _github_ses=A123CdE; path=/")
  end

  # Backwards-compatibility - previously, #headers returned a Hash
  it "responds to hash methods" do
    response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
    hash_methods = {}.methods - response.headers.methods
    hash_methods.each do |method_name|
      expect(response.headers.respond_to?(method_name)).to be_truthy
    end
  end

  describe "#is_a?" do
    subject { HTTParty::Response.new(@request_object, @response_object, @parsed_response) }

    it { is_expected.to respond_to(:is_a?).with(1).arguments }
    it { expect(subject.is_a?(HTTParty::Response)).to be_truthy }
    it { expect(subject.is_a?(Object)).to be_truthy }
  end

  describe "#kind_of?" do
    subject { HTTParty::Response.new(@request_object, @response_object, @parsed_response) }

    it { is_expected.to respond_to(:kind_of?).with(1).arguments }
    it { expect(subject.kind_of?(HTTParty::Response)).to be_truthy }
    it { expect(subject.kind_of?(Object)).to be_truthy }
  end

  describe "semantic methods for response codes" do
    def response_mock(klass)
      response = klass.new('', '', '')
      allow(response).to receive(:body)
      response
    end

    context "major codes" do
      it "is information" do
        net_response = response_mock(Net::HTTPInformation)
        response = HTTParty::Response.new(@request_object, net_response, '')
        expect(response.information?).to be_truthy
      end

      it "is success" do
        net_response = response_mock(Net::HTTPSuccess)
        response = HTTParty::Response.new(@request_object, net_response, '')
        expect(response.success?).to be_truthy
      end

      it "is redirection" do
        net_response = response_mock(Net::HTTPRedirection)
        response = HTTParty::Response.new(@request_object, net_response, '')
        expect(response.redirection?).to be_truthy
      end

      it "is client error" do
        net_response = response_mock(Net::HTTPClientError)
        response = HTTParty::Response.new(@request_object, net_response, '')
        expect(response.client_error?).to be_truthy
      end

      it "is server error" do
        net_response = response_mock(Net::HTTPServerError)
        response = HTTParty::Response.new(@request_object, net_response, '')
        expect(response.server_error?).to be_truthy
      end
    end

    context "for specific codes" do
      SPECIFIC_CODES = {
        accepted?:                        Net::HTTPAccepted,
        bad_gateway?:                     Net::HTTPBadGateway,
        bad_request?:                     Net::HTTPBadRequest,
        conflict?:                        Net::HTTPConflict,
        continue?:                        Net::HTTPContinue,
        created?:                         Net::HTTPCreated,
        expectation_failed?:              Net::HTTPExpectationFailed,
        forbidden?:                       Net::HTTPForbidden,
        found?:                           Net::HTTPFound,
        gateway_time_out?:                Net::HTTPGatewayTimeOut,
        gone?:                            Net::HTTPGone,
        internal_server_error?:           Net::HTTPInternalServerError,
        length_required?:                 Net::HTTPLengthRequired,
        method_not_allowed?:              Net::HTTPMethodNotAllowed,
        moved_permanently?:               Net::HTTPMovedPermanently,
        multiple_choice?:                 Net::HTTPMultipleChoice,
        no_content?:                      Net::HTTPNoContent,
        non_authoritative_information?:   Net::HTTPNonAuthoritativeInformation,
        not_acceptable?:                  Net::HTTPNotAcceptable,
        not_found?:                       Net::HTTPNotFound,
        not_implemented?:                 Net::HTTPNotImplemented,
        not_modified?:                    Net::HTTPNotModified,
        ok?:                              Net::HTTPOK,
        partial_content?:                 Net::HTTPPartialContent,
        payment_required?:                Net::HTTPPaymentRequired,
        precondition_failed?:             Net::HTTPPreconditionFailed,
        proxy_authentication_required?:   Net::HTTPProxyAuthenticationRequired,
        request_entity_too_large?:        Net::HTTPRequestEntityTooLarge,
        request_time_out?:                Net::HTTPRequestTimeOut,
        request_uri_too_long?:            Net::HTTPRequestURITooLong,
        requested_range_not_satisfiable?: Net::HTTPRequestedRangeNotSatisfiable,
        reset_content?:                   Net::HTTPResetContent,
        see_other?:                       Net::HTTPSeeOther,
        service_unavailable?:             Net::HTTPServiceUnavailable,
        switch_protocol?:                 Net::HTTPSwitchProtocol,
        temporary_redirect?:              Net::HTTPTemporaryRedirect,
        unauthorized?:                    Net::HTTPUnauthorized,
        unsupported_media_type?:          Net::HTTPUnsupportedMediaType,
        use_proxy?:                       Net::HTTPUseProxy,
        version_not_supported?:           Net::HTTPVersionNotSupported
      }

      if ::RUBY_PLATFORM != "java"
        SPECIFIC_CODES[:multiple_choices?] = Net::HTTPMultipleChoices
      end

      # Ruby 2.6, those status codes have been updated.
      if ::RUBY_PLATFORM != "java"
        SPECIFIC_CODES[:gateway_timeout?]       = Net::HTTPGatewayTimeout
        SPECIFIC_CODES[:payload_too_large?]     = Net::HTTPPayloadTooLarge
        SPECIFIC_CODES[:request_timeout?]       = Net::HTTPRequestTimeout
        SPECIFIC_CODES[:uri_too_long?]          = Net::HTTPURITooLong
        SPECIFIC_CODES[:range_not_satisfiable?] = Net::HTTPRangeNotSatisfiable
      end

      SPECIFIC_CODES.each do |method, klass|
        it "responds to #{method}" do
          net_response = response_mock(klass)
          response = HTTParty::Response.new(@request_object, net_response, '')
          expect(response.__send__(method)).to be_truthy
        end
      end
    end
  end

  describe "headers" do
    let (:empty_headers) { HTTParty::Response::Headers.new }
    let (:some_headers_hash) do
      {'Cookie' => 'bob',
      'Content-Encoding' => 'meow'}
    end
    let (:some_headers) do
      HTTParty::Response::Headers.new.tap do |h|
        some_headers_hash.each_pair do |k,v|
          h[k] = v
        end
      end
    end
    it "can initialize without headers" do
      expect(empty_headers).to eq({})
    end

    it 'always equals itself' do
      expect(empty_headers).to eq(empty_headers)
      expect(some_headers).to eq(some_headers)
    end

    it 'does not equal itself when not equivalent' do
      expect(empty_headers).to_not eq(some_headers)
    end

    it 'does equal a hash' do
      expect(empty_headers).to eq({})

      expect(some_headers).to eq(some_headers_hash)
    end
  end

  describe "#tap" do
    it "is possible to tap into a response" do
      result = @response.tap(&:code)

      expect(result).to eq @response
    end
  end

  describe "#inspect" do
    it "works" do
      inspect = @response.inspect
      expect(inspect).to include("HTTParty::Response:0x")
      expect(inspect).to include("parsed_response={\"foo\"=>\"bar\"}")
      expect(inspect).to include("@response=#<Net::HTTPOK 200 OK readbody=false>")
      expect(inspect).to include("@headers={")
      expect(inspect).to include("last-modified")
      expect(inspect).to include("content-length")
    end
  end

  describe 'marshalling' do
    before { RSpec::Mocks.space.proxy_for(@response_object).remove_stub(:body) }

    specify do
      marshalled = Marshal.load(Marshal.dump(@response))

      expect(marshalled.headers).to eq @response.headers
      expect(marshalled.body).to eq @response.body
      expect(marshalled.code).to eq @response.code
    end
  end
end