Testing Faraday clients for APIs in your Rails app
I am working on two applications that extensively communicate with each other using a JSON API. One application is for inventory management where products and stock quantity gets management. The other application is a web store frontend. Both are Ruby on Rails applications.
The inventory management application exposes the JSON API so I can do realtime data synchronization in the web store frontend. For this, I am using Faraday as the http client.
Here's a sample of a call:
class LineItem < ActiveRecord::Base
def create_in_vacustock
connection = vacustock_connection
response = connection.post "/api/v1/delivery_assignments/#{order.external_id}/assignment_items.json", { assignment_item: { ean8: product.external_id, amount: quantity, external_line_item_id: id, custom_code: customer_article_code } }
assignment_item = response.body['assignment_item']
update_attribute(:external_id, assignment_item['id'])
end
private
def vacustock_connection
return Faraday.new(SystemSettings.vacustock_endpoint_url, :params => { :user_credentials => SystemSettings.vacustock_api_key} ) do |faraday|
faraday.request :url_encoded
faraday.response :json
faraday.adapter Faraday.default_adapter
end
end
end
The create_in_vacustock method is used to create an AssignmentItem in the inventory application using a POST request with some JSON as parameters.
You can see that I created a private method vacustock_connection that actually initializes the Faraday client. This is important for two reasons. First, I can re-use that method and initialize connections for the update_in_vacustock and delete_from_vacustock methods as well. These methods are not shown in the sample. Second, I will use that method for stubbing out the Faraday client in my test that I will show you now.
Faraday is awesome for it has built-in stubbing for requests. To use these stubs in our test we could use the test adapter like this and mock it out using [Mocha]:
test "create_in_vacustock creates an assignment item" do
line_item = LineItem.create(amount: 20)
vacustock_connection = Faraday.new do |builder|
builder.response :json
builder.adapter :test do |stubs|
stubs.post("/api/v1/delivery_assignments/1/assignment_items.json", {line_item: { amount: 20 } } ) { [200, {}, nil] }
end
end
line_item.stubs(:vacustock_connection).returns(vacustock_connection)
line_item.create_in_vacustock
end
In this test case, Faraday will check if the stubbed URL is requested by the method create_in_vacustock.
If we wanted to test another case with a different URL endpoint we could refactor initialization of the vacustock_connection Faraday client into a private method on our test class and add another stubbed URL to the list of stubs like so:
test "create_in_vacustock creates an assignment item" do
line_item = LineItem.create(amount: 20)
line_item.stubs(:vacustock_connection).returns(vacustock_connection)
line_item.create_in_vacustock
end
test "update_in_vacustock updates an assignment item" do
line_item = LineItem.create(amount: 30)
line_item.stubs(:vacustock_connection).returns(vacustock_connection)
line_item.update_in_vacustock
end
private
def vacustock_connection
Faraday.new do |builder|
builder.response :json
builder.adapter :test do |stubs|
stubs.post("/api/v1/delivery_assignments/1/assignment_items.json", {line_item: { amount: 20 } } ) { [200, {}, nil] }
stubs.update("/api/v1/delivery_assignments/1/assignment_items/1.json", {line_item: { amount: 30 } } ) { [200, {}, nil] }
end
end
end
There are three problems with this second approach.
- It's not very nice. We are adding stubs for different test cases into this single method which means the test cases won't be fully isolated from each other.
- In the case of the test case for the
updateof the line item we will need to hardcode the endpoint address with it's id for the line item which will not be possible if you're using fixtures or factories to set up the test data. - Using this method it is impossible to stub for varying return results. For instance, if you want to test for a different status code or a different JSON response.
I found a pretty clean solution that I am now using throughout the tests to set up my API connection to VacuStock where stubbed requests can be isolated between test cases and where I can properly set endpoint URLs from the generated fixtures I use.
Here's how it looks in a test case:
test "update_in_vacustock should update an assignment item in vacustock" do
line_item = LineItem.create(amount: 120)
vacustock_connection do |stubs|
stubs.put("/api/v1/delivery_assignments/1/assignment_items/#{line_item.id}.json", { line_item: { amount: 120 } }) { [200, {}, nil]
end
line_item.stubs(:vacustock_connection).returns(vacustock_connection)
line_item.update_in_vacustock
end
You'll see that vacustock_connection has become a method that takes a block where I can define my stubs for this specific test case. I moved this method into my test_helper.rb so this method is accessible for all models and controllers where I do API things. Here it is:
class ActiveSupport::TestCase
# other stuff…
def vacustock_connection
@connection ||= Faraday.new do |builder|
builder.response :json
builder.adapter :test do |stubs|
yield(stubs)
end
end
end
end
When the method is called for the first time, it initializes a new Faraday client with the test adapter using the stubs you defined in your test case. It will set itself to the @connection instance variable.
After the first time it is called, the @connection instance variable will be present and it will just return it.
If you have any questions or feedback about this code, please let me know!
Read this article →
