testing with rspec
Are there any examples of rspec testing with responses and errors?(using webmock of course)
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.
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