Back to the books! Let's test our NYT bestsellers API call, which we can add to a service with the following code:
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.
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:
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:
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.
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.
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:
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:
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:
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.
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
vcr
and webmock
to your Gemfile
's test
group, and then add these lines to rails_helper
:VCR.configure do |c|
c.cassette_library_dir = 'spec/cassettes'
c.hook_into :webmock
c.configure_rspec_metadata!
end
Add the following to tests:
describe Bestseller, :vcr => true do
...
Change VCR config to replace sensitive information with environmental variables:
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