Lesson Weekend

Back to the books! Let's test our NYT bestsellers API call, which we can add to a service with the following code:

services/get_bestsellers.rb
class Bestseller
  def self.get_bestseller_list
    response = RestClient::Request.execute(method: :get, url: 'https://api.nytimes.com/svc/books/v3/lists/combined-print-and-e-book-fiction.json', headers: {api_key: [NYT_API_KEY]})
  end
end

Notice that get_bestseller_list has been called on the Bestseller class, not an instance of the object. This way, we can call Bestseller.get_bestseller_list in our controllers. There's no need to instantiate a new object since we don't have any properties to pass into the API's parameters.

Testing API Calls


Let's write some specs to test this call. First, let's create a new folder in specs called services.

We'll start by ensuring that the API call has a status of 200 OK. 200 is the universal HTTP code for a successful HTTP request. Here's the test:

spec/services/get_bestsellers_spec.rb
require 'rails_helper'

describe Bestseller do
  it "returns a 200 success header when the API call is made" do
    response = Bestseller.get_bestseller_list
    expect(response.code).to(eq(200))
  end
end

This test takes advantage of .code, a built-in RestClient method that returns the status code of the response. So now we know the API call was successful, but it would be helpful to have a better test that ensures that the right content is returned in the response. Here's another test:

spec/services/get_bestsellers_spec.rb
  it "returns books when the API call is made" do
    response = JSON.parse(Bestseller.get_bestseller_list)
    expect(response["results"]["books"]).to_not(eq(nil))
  end

This ensures that the response actually includes ["books"]. It's still not extremely specific. We could, for example, make sure that the .size of ["results"]["books"] is equal to 15, since the documentation for the API states that the call will return 15 books.

This works, but there are a few issues.

  • First, our tests are running very slowly. Each test makes a call to the NYT, which is far more time-intensive than running code locally.

  • Second, we're using calls against our daily call limit; the NYT Books API allows a maximum of 1,000 calls a day. While it's unlikely we'll hit this limit, we shouldn't make calls unless it's absolutely necessary.

  • Third, if the API is experiencing service issues, our tests will break even though there's nothing wrong with our code.

Stubbing with VCR

The way to fix this is to stub our external web requests. In this particular case, our stubs will "fake" the request and return a predetermined response. Don't worry if this isn't quite clear yet; there's a longer description of stubs in the next lesson.

To stub our requests, we'll use a gem called VCR.

Using VCR

Here's how VCR works:

  • The first time we test an API call, the test will go through the full request and response cycle, returning the response from the API.

  • VCR records the API request and response, which it saves as a cassette. In other words, VCR stubs it for future use.

  • When the test is run again, no API call is made. Instead, VCR stubs the response. The test will run much faster as a result.

To use VCR, add vcr and webmock to your Gemfile's test group, and then add these lines to rails_helper.rb:

spec/rails_helper.rb
VCR.configure do |c|
  c.cassette_library_dir = 'spec/cassettes'
  c.hook_into :webmock
  c.configure_rspec_metadata!
end

The lines of code above are in place to configure our VCR cassettes. We start by specifying a directory, then tell VCR to use the webmock configuration, which is the most common configuration for using VCR. The final line allows us to add :vcr to our RSpec tests to specify that RSpec should use VCR and make a cassette for the test.

Now we can add VCR to specs that test HTTP calls. To configure our get_bestseller_spec.rb, we'd add the following:

spec/services/get_bestseller_spec.rb
describe Bestseller, :vcr => true do
...

That's it. VCR will handle the stubbing for us.

There's one other problem we need to address. VCR records our requests and the responses in spec/cassettes, which we'll be checking into Git. However, our api_key and any other sensitive information is contained in the request, which VCR records by default. We need to tell VCR to replace sensitive information with environmental variables:

spec/rails_helper.rb
VCR.configure do |c|
  c.cassette_library_dir = 'spec/cassettes'
  c.hook_into :webmock
  c.configure_rspec_metadata!
  c.filter_sensitive_data('<api_key>') { ENV['NYT_API_KEY'] }
end

The block { ENV['NYT_API_KEY'] } is the text to match and replace while the argument ('<api_key>') is the text that will replace the sensitive information in any cassettes that VCR records.

If we delete the spec/cassettes folder and run our specs again, VCR will scrub our sensitive information.

You should always test code that makes external API calls. It's relatively straightforward to test a simple GET call but many calls can quickly get more complex. In these cases, it's often helpful to use binding.pry() to help you both make and test calls.

Sample API call tests

spec/services/get_bestsellers_spec.rb
require 'rails_helper'

describe Bestseller do
  it "returns a 200 success header when the API call is made" do
    response = Bestseller.get_bestseller_list
    expect(response.code).to(eq(200))
  end

  it "returns books when the API call is made" do
    response = JSON.parse(Bestseller.get_bestseller_list)
    expect(response["results"]["books"]).to_not(eq(nil))
  end

end

Adding VCR

  • Add vcr and webmock to your Gemfile's test group, and then add these lines to rails_helper:
spec/rails_helper.rb
VCR.configure do |c|
  c.cassette_library_dir = 'spec/cassettes'
  c.hook_into :webmock
  c.configure_rspec_metadata!
end

Add the following to tests:

spec/services/get_bestseller_spec.rb
describe Bestseller, :vcr => true do
...

Change VCR config to replace sensitive information with environmental variables:

spec/rails_helper.rb
VCR.configure do |c|
  c.cassette_library_dir = 'spec/cassettes'
  c.hook_into :webmock
  c.configure_rspec_metadata!
  c.filter_sensitive_data('<api_key>') { ENV['NYT_API_KEY'] }
end

Lesson 6 of 27
Last updated August 7, 2022