Testing RESTful Services, Part 1: Test Creation
The scripts are based on the book: API Testing Recipes in Ruby.
Update: Part 2: Run frequently in a Continuous Testing Server.
RESTful web services are based on the Representational State Transfer (REST) architecture. In a RESTful service, payloads are in some uniform format (e.g. JSON, HTML, XML, etc), via HTTP.
I will use a sample REST service site, Thomas Bayer, for examples to test typical RESTful services: LIST, READ, CREATE, UPDATE and DELETE records.
Table of Contents:
∘ My API Testing Approach
· LIST all records
· READ a record
· CREATE a new record
· UPDATE a record
· DELETE a record
· Review (Zhimin)
My API Testing Approach
Here’s how I write API tests (including SOAP, REST and others):
Try the request in a GUI API tool (like Postman, Paw, etc), which can view request data and response data in a user-friendly way.
Once the request data and endpoint details have been confirmed, I will write the test in Ruby. The benefits are being more flexible, reusable and easier to run in a CI/CD pipeline.
We can use HTTP test applications such as Postman and Paw (macOS only) to invoke REST API calls. I prefer Paw for simplicity, which is like the old Postman.
Zhimin: New Postman offers more features; hence, more complex. Many Postman’s new feature, IMO, can be handled better in scripting languages.
LIST all records
HTTP Method: GET
URL: http://thomas-bayer.com/sqlrest/CUSTOMER
In Paw
In Ruby
We use an HTTP library, such as httpclient, to send HTTP requests.
Zhimin: a better HTTP gem (library in Ruby) is rest-client. This is article is more focus on using raw HTTP as possible, therfore, using httpclient.
require 'httpclient'
http = HTTPClient.new
GET retrieves data by a given URL, in this case, http://thomas-bayer.com/sqlrest/CUSTOMER.
# LIST
resp = http.get("http://www.thomas-bayer.com/sqlrest/CUSTOMER")
# (optional) write response contents to file
File.open("/tmp/rest-list-customers.xml", "w").puts(resp.body)
The response (resp
) has the content:
<?xml version="1.0"?>
<CUSTOMERList xmlns:xlink="http://www.w3.org/1999/xlink">
<CUSTOMER xlink:href="http://www.thomas-bayer.com/sqlrest/CUSTOMER/1/">1</CUSTOMER>
<CUSTOMER xlink:href="http://www.thomas-bayer.com/sqlrest/CUSTOMER/2/">2</CUSTOMER>
<CUSTOMER xlink:href="http://www.thomas-bayer.com/sqlrest/CUSTOMER/3/">3</CUSTOMER>
<CUSTOMER xlink:href="http://www.thomas-bayer.com/sqlrest/CUSTOMER/4/">4</CUSTOMER>
<!-- more -->
</CUSTOMERList>
Next, we parse this XML document to do verification, here I will use Ruby’s built-in REXML. Below are two assertions to verify the customer count and the first customer's ID.
require 'rexml'xml_doc = REXML::Document.new(resp.body)
expect(xml_doc.root.elements.size).to be > 10
expect(xml_doc.root.elements.first.text).to eq("1")
Zhimin: the best XML (and HTML) parsing library is Nokogiri.
READ a record
HTTP Method: GET
URL: http://thomas-bayer.com/sqlrest/CUSTOMER/3
In Paw
In Ruby
http = HTTPClient.new
customer_id = 3
get_rest_url = "http://www.thomas-bayer.com/sqlrest/CUSTOMER/#{customer_id}"# call http request
resp = http.get(get_rest_url)
The reason I used a variable customer_id
here is to make it easier to change.
The response (resp
) has the content:
<?xml version="1.0"?><CUSTOMER xmlns:xlink="http://www.w3.org/1999/xlink">
<ID>3</ID>
<FIRSTNAME>Michael</FIRSTNAME>
<LASTNAME>Clancy</LASTNAME>
<STREET>542 Upland Pl.</STREET>
<CITY>San Francisco</CITY>
</CUSTOMER>
Assertion:
# verify field's value
expect(resp.body).to include("<CITY>San Francisco</CITY>")
Now that we can read records, let’s see how to create new ones.
CREATE a new record
HTTP Method: PUT
URL: http://thomas-bayer.com/sqlrest/CUSTOMER
PUT sends record data to the server, to create a new record.
My new customer request data will be:
<CUSTOMER>
<ID>66670</ID>
<FIRSTNAME>P</FIRSTNAME>
<LASTNAME>Sherman</LASTNAME>
<STREET>42 Wallaby Way</STREET>
<CITY>Sydney</CITY>
</CUSTOMER>
Note that the value in the ID field is arbitrary; just try to make yours unique.
In Paw
The example server returns a 403 (Not Authorised) error for a PUT request, but the new record was actually created successfully. This does not affect our testing.
In Ruby
http = HTTPClient.new
create_rest_url = "http://www.thomas-bayer.com/sqlrest/CUSTOMER/"
new_record_xml = <<END_OF_MESSAGE
<CUSTOMER>
<ID>66670</ID>
<FIRSTNAME>P</FIRSTNAME>
<LASTNAME>Sherman</LASTNAME>
<STREET>42 Wallaby Way</STREET>
<CITY>Sydney</CITY>
</CUSTOMER>
END_OF_MESSAGEresp = http.put(create_rest_url, new_record_xml)
puts resp.body
Assertion with by READ-ing the record to verify that the new customer was created successfully.
customer_id = 66670
get_rest_url = "http://www.thomas-bayer.com/sqlrest/CUSTOMER/#{customer_id}"
resp = http.get(get_rest_url)
expect(resp.body).to include("<CITY>Sydney</CITY>")
UPDATE a record
HTTP Method: POST
URL: http://www.thomas-bayer.com/sqlrest/CUSTOMER/66670
Like CREATE, an XML is required as part of the request. However, because it is an UPDATE, the XML should only contain the fields that are going to be updated. i.e. if you are not changing CITY
, then do not include it in the request.
Let’s say I want to update customer P Sherman
’s first name to Paul (ID: 66670). The update request data is:
<CUSTOMER>
<FIRSTNAME>Paul</FIRSTNAME>
</CUSTOMER>
and request URL contains the customer’s ID: http://www.thomas-bayer.com/sqlrest/CUSTOMER/66670
In Paw
In Ruby
To make our automated test reliable, we need to make sure the record exists first. The best way is to create a brand new record first, and then update it. (same for deletion)
http = HTTPClient.new
cid = 66670
update_rest_url = "http://www.thomas-bayer.com/sqlrest/CUSTOMER/#{cid}/" update_xml = <<END_OF_MESSAGE
<CUSTOMER>
<FIRSTNAME>Paul</FIRSTNAME>
</CUSTOMER>
END_OF_MESSAGEresp = http.post(update_rest_url, update_xml)
expect(resp.code).to eq(200) # OK
Assertion, use the READ script to verify the change was successful:
http = HTTPClient.new
customer_id = 66670
get_rest_url = "http://www.thomas-bayer.com/sqlrest/CUSTOMER/#{customer_id}"
resp = http.get(get_rest_url)
expect(resp.body).to include("<FIRSTNAME>Paul</FIRSTNAME>")
DELETE a record
The final operation in this tutorial is DELETE.
HTTP Method: DELETE
URL: http://www.thomas-bayer.com/sqlrest/CUSTOMER/66670
The expected response to a DELETE call is:
<resource xmlns:xlink="http://www.w3.org/1999/xlink">
<deleted>66670</deleted>
</resource>
In Paw
In Ruby
it "Delete a record" do
http = HTTPClient.new
cid = 66670 # an existing record
delete_rest_url = "http://www.thomas-bayer.com/sqlrest/CUSTOMER/#{cid}"
resp = http.delete(delete_rest_url)
expect(resp.body).to include("<deleted>66670</deleted>")
end
Assertion, verify the record was deleted successfully using READ. This time you are expecting the response to not include any field.
http = HTTPClient.new
customer_id = 66670
get_rest_url = "http://www.thomas-bayer.com/sqlrest/CUSTOMER/#{customer_id}"
resp = http.get(get_rest_url)
expect(resp.body).not_to include("<CITY>Sydney</CITY>")
In this article we have gone through the Ruby scripts for LIST, READ, CREATE, UPDATE and DELETE calls.
Review (Zhimin)
My API testing consists of three phases:
Exploring
Use a GUI tool such as Paw or SoapUI to quickly verify the API services manually.Scripting
Based on the request and response data from the Exploring, start to develop automated tests in RSpec (ruby language), usually very quick.Add to Continuous Testing
To run all API tests in a CT server, as daily regression testing. Before that, we often need serious refactoring and stabilization, a topic for the next article.
By the way, I test REST or SOAP in pretty much the same way. Check out my article: Test SOAP Web Services Effectively, Easily and Freely.
Some might think: Postman can do scripting too. Yes, in a very limited way. Let to illustrate with an example. Suppose I want to achieve these:
Get test data by querying a database (SQL)
Go to a website to trigger an event
Invoke a REST API call
Go to a website to download a PDF to verify
Can you do that in Postman? Or, I want to use Ruby’s Nokogiri to parse XML, Faker to generate a random email, or use 3.days.ago
to generate a dynamic date, or run all tests in a BuildWise CT server, …, etc.
Test Scripting in a good scripting language, such as Ruby, provides the best flexibility and fun! Above all, it is proven and totally free!
Further reading: