dwolla-v2-ruby icon indicating copy to clipboard operation
dwolla-v2-ruby copied to clipboard

testing with rspec

Open Tolchi opened this issue 2 years ago • 2 comments

Are there any examples of rspec testing with responses and errors?(using webmock of course)

Tolchi avatar Sep 11 '23 18:09 Tolchi

This is for real a massive struggle. I have positive responses working well enough, but trying to get the error paths to work has been like trying to herd cats.

I have a simple wrapper around the gem for convinence on our system:

module Dwolla
  class Customer
    def request_verified_customer_create(customer_params)
      request_path = "customers"
      # Using DwollaV2 - https://github.com/Dwolla/dwolla-v2-ruby
      request_body = {
        firstName: customer_params[:first_name],
        ...
      }

      begin
        new_customer = client.post request_path, request_body

        {
          status: :success,
          customer_url: new_customer.response_headers[:location],
          resource: request_path
        }
      rescue DwollaV2::ValidationError => e
        # Handle validation errors
        embedded_errors = e._embedded
        errors = embedded_errors["errors"].map do |error|
          {
            field: error["path"],
            message: error["message"]
          }
        end
        {
          status: :error,
          errors: errors
        }
      rescue DwollaV2::Error => e
        SMLogger.error "Dwolla API Error: #{e}"
        {
          status: :error,
          errors: e.message
        }
      end
    end

I know that it is correct since it works in the UI, but testing is bleh.

Here is the happy path I did manage to get done:


RSpec.describe Dwolla::Customer do
  let(:dwolla_client) { described_class.new }
  let(:create_url) { "https://api-sandbox.dwolla.com/customers" }
  let(:customer_id) { "FC451A7A-AE30-4404-AB95-E3553FCD733F" }
  let(:customer_url) { "https://api-sandbox.dwolla.com/customers/#{customer_id}" }
  let(:mock_dwolla_client) { instance_double(DwollaV2::Client) }

  let(:valid_customer_attributes) do
    # The attributes the wrapper receives
    {
      first_name: "John",
      ...
    }
  end
  let(:expected_request_body) do
    # represents how the wrapper transforms the attributes for the API
    {
      firstName: valid_customer_attributes[:first_name],
      ...
    }
  end

  # Valid responses
  let(:customer_create_response) do
    instance_double(
      DwollaV2::Response,
      response_status: 201,
      response_headers: {
        "content-type": "application/vnd.dwolla.v1.hal+json",
        location: customer_url
      }
    )
  end

  before do
    allow(dwolla_client).to receive(:client).and_return(mock_dwolla_client)
    allow(mock_dwolla_client).to receive(:get).with("customers/#{customer_id}").and_return(customer_details_response)
    allow(mock_dwolla_client).to receive(:post).with("customers", hash_including(firstName: valid_customer_attributes[:first_name])).and_return(customer_create_response)
    # allow(mock_dwolla_client).to receive(:post).with("customers", hash_including(firstName: nil)).and_raise(DwollaV2::ValidationError.new(failed_customer_create_response.response))
    # allow(mock_dwolla_client).to receive(:post).with("customers", hash_including(email: nil)).and_raise(DwollaV2::ServerError.new(server_failed_customer_create_response.response))
  end

  describe "#request_verified_customer_create" do
    context "when requesting a personal verified account" do
      it "creates a new customer" do
        response = dwolla_client.request_verified_customer_create(valid_customer_attributes)
        expect(response[:status]).to eq(:success)
        expect(response[:customer_url]).to eq(customer_url)
        expect(response[:resource]).to eq("customers")
      end
    end
  end
end

I've tried everything I could think of to try and get a negative path to work, but to no avail. The only thing left to try is to maybe force it to raise an error but use a local custom response-like object that responds to body with a Hash as the error class expects: https://github.com/Dwolla/dwolla-v2-ruby/blob/dfa4b84ecf7f78db92267762a359f7ae3fb967f7/lib/dwolla_v2/error.rb#L8-L17

But that seems like going too far just to test a negative path.

E1337Kat avatar Apr 23 '25 15:04 E1337Kat

I finally got something that works correctly for the negative path:

RSpec.describe Dwolla::Customer do
  let(:dwolla_client) { described_class.new }
  let(:create_url) { "https://api-sandbox.dwolla.com/customers" }
  let(:customer_id) { "FC451A7A-AE30-4404-AB95-E3553FCD733F" }
  let(:customer_url) { "https://api-sandbox.dwolla.com/customers/#{customer_id}" }
  let(:mock_dwolla_client) { instance_double(DwollaV2::Client) }

  # Invalid responses
  # Stub raw response and let the DwollaV2 client parse it
  # Then we can use the parsed response to create our own error object
  # and raise it in the same way DwollaV2 does
  let!(:failed_customer_create_response) do
    with_webmock_stubbing(allow_localhost: true) do
      stub_request(:post, create_url)
        .with(body: expected_request_body.merge({firstName: nil}))
        .to_return(
          status: 400,
          headers: {"Content-Type" => "application/vnd.dwolla.v1.hal+json"},
          body: {
            code: "ValidationError",
            message: "Validation error(s) present. See embedded errors list for more details.",
            _embedded: {
              errors: [
                {
                  code: "Required",
                  message: "FirstName required.",
                  path: "/firstName",
                  _links: {}
                }
              ]
            }
          }.to_json
        )
    end
  end

  before do
    allow(dwolla_client).to receive(:client).and_return(mock_dwolla_client)
    the_response = failed_customer_create_response.response
    # This is the most important part, as this transforms the body in the same way the DwollaV2::Error class does to allow the calling of body keys as if they are methods on the body object.
    the_morphed_body = DwollaV2::Util.deep_super_hasherize(DwollaV2::Util.deep_parse_iso8601_values(JSON.parse(the_response.body, symbolize_names: true)))
    # We then replace the response with this controlled object so that we can replace the body on the local response without actually screwing with the webmock response.
    allow(failed_customer_create_response).to receive(:response).and_return(the_response)
    allow(the_response).to receive(:body).and_return(the_morphed_body)
    allow(mock_dwolla_client).to receive(:post).with("customers", hash_including(firstName: nil)).and_raise(DwollaV2::ValidationError.new(failed_customer_create_response.response))
    allow(mock_dwolla_client).to receive(:post).with("customers", hash_including(email: nil)).and_raise(DwollaV2::ServerError.new(server_failed_customer_create_response.response))
  end

  describe "#request_verified_customer_create failure" do
    it "handles validation errors" do
      invalid_attributes = valid_customer_attributes.merge(first_name: nil)
      response = dwolla_client.request_verified_customer_create(invalid_attributes)
      expect(response[:status]).to eq(:error)
      expect(response[:errors]).not_to be_empty
    end
  end
end

E1337Kat avatar Apr 25 '25 17:04 E1337Kat