September 12, 2019

Are you sure the buses are still listed? Interacting with APIs with Ruby + NET::HTTP + Gauge

This is Part 2 of 2 of a blog series. Care to go back to the beginning

There are many different Ruby libraries that allow you to interact with an API:

  • Net/Http: The HTTP client built into Ruby standard library. 
  • Httparty: Built on top of Net/Http by John Nunemaker, you can GET the HTTP Response HTTP Code, Response Message, and the HTTP Headers with one call. The Google Group was last active in 2017. 
  • Faraday: Also allows you to get the status, headers, and body, allowing you a bit more to customize the HTTP request
  • Rest-Client: a "simple HTTP and REST client for Ruby, inspired by the Sinatra’s microframework style of specifying actions: get, put, post, delete".
For this sample project, where we are simply getting data from the MBTA API, we will use:

  • Net/HTTP to get data from the API
  • The JSON library to parse the data
  • The test/unit library to assert that the expected values and the actual values match up
  • ThoughtWorks Gauge as the test framework. 

Before we make the test data-driven, using the ThoughtWorks Gauge test framework and the table of data in our last segment of this blog, let's focus on one single data point...

What Data Can We Retrieve For Bus Route 230? 


Let's say we wanted to access all the information the Massachusetts Bay Transportation Authority (MBTA) has for the Route 230 bus route:

According to the MBTA V2 API developers documentation, since there is no API Key we need to use, we could open up Google Chrome and go to the URL, https://api-v3.mbta.com/routes/230:

 {  
  "data": {  
   "attributes": {  
    "color": "FFC72C",  
    "description": "Local Bus",  
    "direction_destinations": [  
     "Montello Commuter Rail Station",  
     "Quincy Center"  
    ],  
    "direction_names": [  
     "Outbound",  
     "Inbound"  
    ],  
    "fare_class": "Local Bus",  
    "long_name": "Montello Commuter Rail Station - Quincy Center",  
    "short_name": "230",  
    "sort_order": 52300,  
    "text_color": "000000",  
    "type": 3  
   },  

How could we do this programmatically?

Create an HTTP Request with Net::HTTP::Get

According to ToolsQA.com, an "HTTP Request is a packet of Information that one computer sends to another computer to communicate something. To its core, HTTP Request is a packet of binary data sent by the Client to server".

As part of the HTTP Request, we can see if we can GET information stored in the Uniform Resource Locator, https://api-v3.mbta.com/routes/230.

  • View more information about HTTP Methods such as GET, POST, PUT, PATCH at the W3Schools

After requiring the Net/Http library, we convert the URL into a URI using the Ruby gem of the same name. The URI Ruby gem goes to the URL and breaks it into Uniform Resource Identifier components such as the hostname and the port name.

The URI information is then passed into a new HTTP Request.

 uri = URI("https://api-v3.mbta.com/routes/230  
 request = Net::HTTP::Get.new(uri)  

Set Up HTTP Request Options to SSL and HTTPS


The MBTA API uses SSL a, "Secure Sockets Layer and, in short, it's the standard technology for keeping an internet connection secure and safeguarding any sensitive data that is being sent between two systems, preventing criminals from reading and modifying any information transferred, including potential personal details", according to Symantec.

If we just connected with the Net/Http gem using HTTP instead of HTTPS, we would not be able to retrieve data. For the Net/Http gem, we need to turn SSL options on, detailing what scheme the URI should be:

 req_options = {  
   use_ssl: uri.scheme == "https"  
  }  

Feed Net/Http the HTTP Request, Get the HTTP Response Code


Now that we have crafted the HTTP Request, and added HTTPS as the scheme as part of the required options, we can feed both into Net/Http, saving the HTTP Response in a variable we can call response:

  response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|  
   http.request(request)  
  end  


HTTP response status codes indicate whether a specific HTTP request has been successfully completed

Was everything successful? We could find out by printing out the HTTP Response Status codes to the screen:

puts response.code
  • 200 if successful
  • 401 if unauthorized
  • 404 if not found
  • 500 if internal service error
With our test, we only want it to pass if the response.code is actually "200". Let's use the Ruby gem 'test/unit' to check to see if the expected "200" and actual values (response.code) are equal:

assert_equal("200", response.code)  


Examine the Response Body To Check the Long Name of the 230 Bus Route


If we were to print the entire JSON response.body, all text shown in https://api-v3.mbta.com/routes/230 would be displayed.

If we only wanted to collect all the data attribute for route 230, we could use the JSON library to parse those out into a variable called attributes:

 attributes = JSON.parse(response.body)['data']['attributes']  

If we then wanted to search these stored attributes hashed together to find the long_name, we could search for the value that matched the key, long_name, by using the code: attributes['long_name'].

 assert_equal("Montello Commuter Rail Station - Quincy Center", attributes['long_name'])  

Add in ThoughtWorks Gauge

Previously, we created a data table using ThoughtWorks Gauge in a specification file:

routes_long_name.spec:
 # ROUTE: Verify that the Long Name is Returned  
   
 Given the ID, verify that the route api returns the  
 long_name of the route.  
   
 |route| long_name                         |  
 |-----------------------------------------|  
 | 210 | Quincy Center - Fields Corner     |  
 | 212 | Quincy Center - North Quincy      |  
 | 220 | Hingham Depot - Quincy Center     |  
 | 222 | East Weymouth - Quincy Center     |  
 | 230 | Montello Commuter Rail Station - Quincy Center |  
 | 236 | South Shore Plaza - Quincy Center |  
   
 ## Given the route id return the long_name  
 * Passing <route> to ROUTES api returns <long_name>  

For each entry listed above, the route and long name would be passed into the step, Passing <route> to ROUTES api returns <long_name>.

Following the code we listed above, our step_implementation would look like:

find_longname_spec.rb
 require 'net/http'  
 require 'test/unit'  
 require 'json'  
 include Test::Unit::Assertions  
   
 step 'Passing <route> to ROUTES api returns <long_name>', continue_on_failure: true do |route, long_name|  
  uri = URI("https://api-v3.mbta.com/routes/#{route}")  
  puts "Connecting to: #{uri}"  
   
  request = Net::HTTP::Get.new(uri)  
   
  req_options = {  
   use_ssl: uri.scheme == "https"  
  }  
   
  response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|  
   http.request(request)  
  end  
   
  assert_equal("200", response.code)  
   
  attributes = JSON.parse(response.body)['data']['attributes']  
  assert_equal(long_name, attributes['long_name'])  
 end  


... And that's how you can create data-driven API Testing using Ruby's Net/HTTP Gem! Want to try it out yourself? Download the code off my GitHub site, gauge-ruby-api.


Are you sure the buses are still listed? 
Data-driven API tests with Ruby + NET::HTTP + ThoughtWorks Gauge:


Happy Testing!

-T.J. Maher
Sr. QA Engineer, Software Engineer in Test
Meetup Organizer, Ministry of Testing - Boston

Twitter | YouTubeLinkedIn | Articles

No comments: