Tutorial Part 3 - REST API Testing
In this part of the tutorial, you will use Boa Constrictor to automate REST API tests for Dog API, a public API for random pictures of dogs. The first set of tests will show the basics of Boa Constrictor’s REST API interactions, while the second set of tests will show advanced techniques.
RestSharp: Boa Constrictor’s REST API interactions use RestSharp.
Prerequisite: Make sure you already completed both Part 1 and Part 2. Part 3 expects you to use the same example project and to already know Screenplay concepts.
Basic Interactions
Boa Constrictor adapts RestSharp into the Screenplay Pattern. Basic Screenplay calls are thin wrappers around RestSharp calls. Making REST API calls through Boa Constrictor provides automatic logging and enables them to be called by other interactions.
1. Creating a Test Class with an Actor
Inside the Boa.Constrictor.Example
project,
create a new file in the Tests
directory named ScreenplayRestApiBasicTest.cs
.
Add the following code to the file:
using Boa.Constrictor.Screenplay;
using NUnit.Framework;
namespace Boa.Constrictor.Example
{
public class ScreenplayRestApiBasicTest
{
private IActor Actor;
[SetUp]
public void InitializeScreenplay()
{
Actor = new Actor(name: "Andy", logger: new ConsoleLogger());
}
}
}
ScreenplayRestApiBasicTest
will contain the NUnit test cases for the first half of Part 3.
Each test case should use its own Actor,
which is initialized in the [SetUp]
method.
The line to create the Actor is the same as
the Actor line from ScreenplayWebUiTest
.
2. Adding REST API Abilities
Add the following import statements to ScreenplayRestApiBasicTest
:
using Boa.Constrictor.RestSharp;
using RestSharp;
Then, add this line to the [SetUp]
method:
Actor.Can(CallRestApi.Using(new RestClient("https://dog.ceo/")));
Boa Constrictor’s CallRestApi
Ability enables Actors to call REST APIs using RestSharp.
It is part of the Boa.Constrictor.RestSharp
namespace.
Let’s unpack how this Ability works:
Code | Purpose |
---|---|
Actor.Can(...) |
Adds the given Ability to the Actor. |
CallRestApi.Using(...) |
Constructs the Ability with the given RestSharp client. |
RestClient |
A RestSharp class for REST API clients that holds information (like base URL and authentication) and executes requests. |
"https://dog.ceo/" |
Dog API’s base URL. |
In this line, the actor
Actor is given a CallRestApi
Ability with a RestClient
object that targets "https://dog.ceo/"
.
The code for this Ability should look similar to the code for adding Web UI Abilities.
Authentication: Boa Constrictor does not handle authentication, but RestSharp does. You can add authenticators directly to the RestSharp client.
3. Modeling Requests
The REST API endpoint we want to test is the
HTTP GET
method for
https://dog.ceo/api/breeds/image/random.
It should yield a response like this:
{
"message": "https://images.dog.ceo/breeds/schipperke/n02104365_9489.jpg",
"status": "success"
}
Since this is a basic GET
request with no headers or body,
you could even enter the absolute URL
directly into your web browser
to test the response.
The response has two parts:
Field | Value |
---|---|
message |
A hyperlink to a random picture of a dog. This link should be different every time the request is called. |
status |
A string indicating the success-or-failure status of fetching the image link. |
RestSharp uses the RestRequest
object
for creating requests that the client executes.
RestRequest
supports all types of request fields
headers, parameters, bodies, etc.
Boa Constrictor does not add anything on top - it uses RestRequest
directly for interactions.
Requests can be long. Many tests may need to call the same requests, too. As a best practice, requests typically should not be written in-line where they are called. Instead, they should be written in separate classes as builder methods so that they can be reused anywhere, just like locators for web elements.
Create a new directory in the Boa.Constrictor.Example
project named Requests
.
Inside this new folder, create a new file named DogRequests.cs
with the following content:
using RestSharp;
namespace Boa.Constrictor.Example
{
public static class DogRequests
{
public static RestRequest GetRandomDog() =>
new RestRequest("api/breeds/image/random", Method.Get);
}
}
DogRequests
is a class that contains builder methods for RestRequest
objects.
Like page classes with locators, it is static so that it does not maintain any state of its own.
It only provides builders.
The GetRandomDog
method constructs a new RestSharp request.
The request’s method is GET
, and its resource path is api/breeds/image/random
.
You can use RestRequest
’s fluent syntax to add other fields, like headers and parameters.
Builder methods like this should have descriptive names and declarative bodies.
They may also take in arguments to customize parts of the request, such as IDs or request parameter values.
4. Calling Basic Requests
For our first test, we will call the Dog API endpoint and verify a successful response.
Add the following test stub to ScreenplayRestApiBasicTest
:
[Test]
public void TestDogApiStatusCode()
{
}
To call the endpoint, add the following code to the test:
var request = DogRequests.GetRandomDog();
var response = Actor.Calls(Rest.Request(request));
The first line builds the request object. The second line calls the request using a Boa Constrictor interaction. Read that second line in plain English: “The actor calls the REST request to get a random dog.” Let’s break it down:
Code | Purpose |
---|---|
response |
The RestResponse object returned by the REST API call. |
Actor.Calls(...) |
Calls any type of interaction. Alias for Actor.AsksFor(...) or Actor.AttemptsTo(...) . |
Rest.Request(...) |
The Question that calls the given request. Under the hood, it uses the Ability’s RestClient object to execute the given RestRequest object. |
request |
The RestRequest object for calling the Dog API endpoint. |
The Rest
class shown in the code is actually syntactic Screenplay sugar.
Boa Constrictor’s REST requests are actually RestApiCall
Questions that return RestResponse
answers.
The two lines below are equivalent:
// The concise, readable way to call REST APIs
Actor.Calls(Rest.Request(request));
// The "traditional" way to call REST APIs
Actor.AsksFor(new RestApiCall(request));
Warning:
Do not try to call RestApiCall
directly.
Its constructor is not public.
The example above serves only to show how REST requests follow the Screenplay Pattern.
Use Rest.Request(...)
to call REST API interactions.
All REST requests return RestSharp RestResponse
objects.
Just like for requests, Boa Constrictor does not add anything to RestSharp’s responses.
It simply passes response objects through the interaction.
Check the RestSharp docs for RestResponse
to for more info.
The simplest way to verify if the call was successful is to check the response code.
The recommended assertion library to use with Boa Constrictor is
Fluent Assertions.
To check the status code, add the following import statements to ScreenplayRestApiBasicTest
:
using FluentAssertions;
using System.Net;
Then, add this line to TestDogApiStatusCode
:
response.StatusCode.Should().Be(HttpStatusCode.OK);
The completed test case should now look like this:
[Test]
public void TestDogApiStatusCode()
{
var request = DogRequests.GetRandomDog();
var response = Actor.Calls(Rest.Request(request));
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
Build and run the test.
It should take about 1 second to execute, and it should pass.
If you want to make sure the assertion is really working,
you can temporarily change it to Should().NotBe(HttpStatusCode.OK)
and watch the test fail.
5. Deserializing Response Bodies
Checking a response’s status code is a valuable assertion,
but checking a response’s content is arguably more important.
Most response bodies are formatted using JSON or XML.
For convenience,
RestSharp can automatically deserialize responses.
Deserialized objects make it possible to check fields in response bodies,
such as the message
and status
strings from the Dog API responses.
To deserialize the content of an RestResponse
object,
RestSharp needs a class with properties or instance variables that match the structure of the response body.
Create a new directory named Responses
under the Boa.Constrictor.Example
project.
Then, create a new file named DogResponses.cs
in this folder with the following code:
namespace Boa.Constrictor.Example
{
public class DogResponse
{
public string Message { get; set; }
public string Status { get; set; }
}
}
DogResponse
is the serialization class for Dog API’s GET
response.
Notice how its properties mirror the structure of the API’s actual JSON response:
{
"message": "https://images.dog.ceo/breeds/schipperke/n02104365_9489.jpg",
"status": "success"
}
Let’s write a new test case to show how to use DogResponse
for deserializing responses.
Add the following test stub to ScreenplayRestApiBasicTest
:
[Test]
public void TestDogApiContent()
{
}
Next, add the following code to the new test:
var request = DogRequests.GetRandomDog();
var response = Actor.Calls(Rest.Request<DogResponse>(request));
Compare this call to the one from the previous test, TestDogApiStatusCode
.
The only difference is the DogResponse
type generic tacked onto Rest.Request<DogResponse>(...)
.
Adding the type generic makes the interaction automatically deserialize the response body into the given type.
Boa Constrictor simply passes it through to RestSharp.
The response object will by typed as RestResponse<DogResponse>
,
and it will have a special member named Data
that is the DogResponse
object parsed from the response’s body.
Finally, add assertions to the test:
response.StatusCode.Should().Be(HttpStatusCode.OK);
response.Data.Status.Should().Be("success");
response.Data.Message.Should().NotBeNullOrWhiteSpace();
The first assertion is the same status code check.
However, the second and third assertions check values in response.Data
.
The status should indicate success, and the message should contain a URL to a random image.
Custom Serializers:
RestSharp lets you provide custom serializers
for request and response bodies.
You can use serializers provided by RestSharp,
such as one for Json.Net,
or you can implement your own.
Simply add custom serializers directly to the RestSharp client object before adding the CallRestApi
Ability to the Actor.
Troubleshooting: Requests can be tricky to call and deserialize properly. Make sure to specify all parts carefully.
The completed test should look like this:
[Test]
public void TestDogApiContent()
{
var request = DogRequests.GetRandomDog();
var response = Actor.Calls(Rest.Request<DogResponse>(request));
response.StatusCode.Should().Be(HttpStatusCode.OK);
response.Data.Status.Should().Be("success");
response.Data.Message.Should().NotBeNullOrWhiteSpace();
}
Build and run the test. It should pass.
Test Duplication:
This tutorial added two very similar tests to ScreenplayRestApiBasicTest
.
TestDogApiContent
essentially supersedes TestDogApiStatusCode
.
In a real-world test project, TestDogApiStatusCode
should arguably be removed.
However, this tutorial project retains both to provide side-by-side examples of calling REST APIs with and without deserializing responses.
Advanced Interactions
The first half of Tutorial Part 3 shows how to call basic REST APIs using Boa Constrictor.
The ScreenplayRestApiBasicTest
tests showed how to call the Dog API endpoint to get a random image of a dog.
However, endpoint did not return an image file - it merely returned a hyperlink to an image file.
Downloading the actual image requires some advanced REST API techniques, which will be covered next.
6. Creating Another Test Class
Let’s create a second test class for “advanced” tests.
Create a class named ScreenplayRestApiAdvancedTest.cs
under the Tests
folder,
and add the following code:
using Boa.Constrictor.RestSharp;
using Boa.Constrictor.Screenplay;
using FluentAssertions;
using NUnit.Framework;
using RestSharp;
using System;
using System.Net;
namespace Boa.Constrictor.Example
{
public class ScreenplayRestApiAdvancedTest
{
private IActor Actor;
[SetUp]
public void InitializeScreenplay()
{
Actor = new Actor(name: "Andy", logger: new ConsoleLogger());
}
}
}
This initial code stub should look very similar to the stub for the basic tests.
Imports:
For convenience, this stub includes all the using
statements from the start.
The previous steps showed which parts come from which namespaces.
7. Calling Multiple Base URLs
Downloading the image file for the randomly-selected dog poses a problem. Compare the base URLs (in bold text below) between the Dog API and the image hyperlink:
Service | URL |
---|---|
Dog API | https://dog.ceo/api/breeds/image/random |
Dog Image API | https://images.dog.ceo/breeds/schipperke/n02104365_9489.jpg |
Unfortunately, the base URLs are different. They will need to use different RestSharp clients. Unfortunately, the Actor from the basic tests was set up to use only one RestSharp client. We need to enable the Actor to use multiple clients.
Overriding Base URLs:
RestSharp allows request objects to override the client’s base URL.
You could use an RestClient
client with the https://dog.ceo/
base URL to execute a RestRequest
request
whose resource is the absolute url https://images.dog.ceo/breeds/schipperke/n02104365_9489.jpg
.
However, this is not good practice because it can make automation code confusing to understand.
The best way to enable Actors to call REST APIs with multiple base URLs is to create a custom Ability for each.
Each Ability can have its own RestClient
object.
Then, interactions can choose the Ability to use via type generics.
Start by creating a new directory named Abilities
in the Boa.Constrictor.Example
project.
In this new folder, add two new classes named CallDogApi.cs
and CallDogImagesApi.cs
.
Add the following code to CallDogApi.cs
:
using Boa.Constrictor.RestSharp;
using RestSharp;
namespace Boa.Constrictor.Example
{
public class CallDogApi : AbstractRestSharpAbility
{
public const string BaseUrl = "https://dog.ceo/";
private CallDogApi(RestClient client) :
base(client) { }
public static CallDogApi UsingBaseUrl() =>
new CallDogApi(new RestClient(BaseUrl));
}
}
And add the following code to CallDogImagesApi.cs
:
using Boa.Constrictor.RestSharp;
using RestSharp;
namespace Boa.Constrictor.Example
{
public class CallDogImagesApi : AbstractRestSharpAbility
{
public const string BaseUrl = "https://images.dog.ceo/";
private CallDogImagesApi(RestClient client) :
base(client) { }
public static CallDogImagesApi UsingBaseUrl() =>
new CallDogImagesApi(new RestClient(BaseUrl));
}
}
Both of these new classes are custom Abilities.
They extend AbstractRestSharpAbility
, which provides helpful properties like a RestClient
object.
They also declare base URLs as constants for convenience.
Their builder methods use the base URLs to construct RestClient
objects that get passed through to the base class’s constructor.
Hard-Coded URLs: Hard-coding URLs in code is typically not a good practice. Typically, automated tests need to run against different environments, which use different base URLs. Tests should read URLs as inputs from an external source (like a config file). This tutorial hard-codes URLs merely for simplicity.
Next, add these new Abilities to the Actor.
Add the following code to the [SetUp]
method in ScreenplayRestApiAdvancedTest
:
Actor.Can(CallDogApi.UsingBaseUrl());
Actor.Can(CallDogImagesApi.UsingBaseUrl());
Now, the Actor can use two different RestSharp clients!
The CallRestApi
Ability from the basic tests
is a “default” or “generic” RestSharp Ability,
whereas the CallDogApi
and CallDogImagesApi
are “custom” Abilities.
The Actor could directly access the RestClient
objects through the Abilities like this:
var dogClient = Actor.Using<CallDogApi>().Client;
var dogImagesClient = Actor.Using<CallDogImagesApi>().Client;
However, the Actor should avoid calling these clients directly. Instead, it should use Boa Constrictor’s REST request Question like this:
var response =
Actor.Calls(
Rest<CallDogApi>
.Request<DogResponse>(
DogRequests.GetRandomDog()));
Let’s unpack this line:
Code | Purpose |
---|---|
response |
The RestResponse object returned by the REST API call. |
Actor.Calls |
Calls any type of interaction. |
Rest<CallDogApi> |
The builder class for REST API interactions. Uses a type generic to specify the RestSharp Ability (e.g. <CallDogApi> ) to use when executing the given request. |
Request<DogResponse> |
The builder method that creates a Question to call a RestSharp request and deserialize the response as a DogResponse object. |
DogRequests.GetRandomDog() |
The builder method that creates an RestRequest object for calling the Dog API endpoint. |
The only difference between this call and the call from the
basic test in Step 5
is that the Rest
builder class bears a type generic for the Ability to use.
Let’s use this new call in a test.
Add this new test to ScreenplayRestApiAdvancedTest
to test the new Abilities:
[Test]
public void TestDogApi()
{
var request = DogRequests.GetRandomDog();
var response = Actor.Calls(Rest<CallDogApi>.Request<DogResponse>(request));
response.StatusCode.Should().Be(HttpStatusCode.OK);
response.Data.Status.Should().Be("success");
response.Data.Message.Should().NotBeNullOrWhiteSpace();
}
Build the project and run the tests.
All tests should pass.
The next step will add a test that uses CallDogImagesApi
.
Should I make a custom RestSharp Ability?
If you only need to call APIs with one base URL,
then you can use the “default” CallRestApi
Ability and avoid adding type generics.
However, if you need to use multiple base URLs,
or if you think you might need to do so in the future,
then create custom RestSharp Abilities.
8. Downloading Files
In addition to receiving JSON and XML bodies, REST requests can also receive file data. For example, a REST API call could download the random dog image given by the Dog API. Files are just another type of response body.
Boa Constrictor provides a special Question for downloading files. Take a look at the following code:
// Image endpoint: https://images.dog.ceo/breeds/schipperke/n02104365_9489.jpg
var imageRequest = new RestRequest("breeds/schipperke/n02104365_9489.jpg");
byte[] imageData = Actor.Calls(Rest<CallDogImagesApi>.Download(imageRequest, ".jpg"));
These lines download the image file as an array of bytes. Let’s break them down:
Code | Purpose |
---|---|
byte[] imageData |
The raw data for the downloaded file. |
Actor.Calls |
Calls any type of interaction. |
Rest<CallDogImagesApi> |
The builder class for REST API interactions using CallDogImagesApi . |
Download |
A builder method for the Question to download the file given by the request. |
imageRequest |
A RestRequest object whose resource is the path to the image. |
".jpg" |
The extension for the file to download. |
Just like Rest.Request
,
Rest.Download
is syntactic Screenplay sugar.
Underneath, it constructs a RestApiDownload
Question.
Once the file is downloaded as a byte array,
its contents could be checked with assertions,
or it could be piped to other destinations.
File Data: When Boa Constrictor downloads a file, the file data is stored in memory. The file is not automatically saved to the file system by default. Step 11 shows how to configure RestSharp Abilities to automatically save downloads to the file system.
Let’s write a test to verify that the image can be successfully downloaded. The test should have the following steps:
- Call the Dog API to get a random dog image link.
- Call the Dog Images API to download the image file at the hyperlink returned by the Dog API request.
- Verify that the file contents are not empty.
The code below implements this test.
Add it to ScreenplayRestApiAdvancedTest
:
[Test]
public void TestDogApiImage()
{
// Call the Dog API to get a random dog image link
var request = DogRequests.GetRandomDog();
var response = Actor.Calls(Rest<CallRestApi>.Request<DogResponse>(request));
// Call the Dog Images API to download the image file
var resource = new Uri(response.Data.Message).AbsolutePath;
var imageRequest = new RestRequest(resource);
var extension = System.IO.Path.GetExtension(resource);
var imageData = Actor.Calls(Rest<CallRestImagesApi>.Download(imageRequest, extension));
// Verify that the file contents are not empty
imageData.Should().NotBeNullOrEmpty();
}
The tricky part about this test is using the values in the first request’s response to create the second request.
new Uri(response.Data.Message).AbsolutePath
parses the resource path from the full hyperlink returned by Dog API.
System.IO.Path.GetExtension(resource)
gets the file extension from the resource.
Download Assertions: Checking that the file is not empty is a weak assertion. It would not determine if the file is incorrect or corrupted. A real-world test should try stricter assertions.
Response Assertions:
Rest.Download
returns only the file data.
It does not capture other parts of the REST response.
If you need to perform assertions on other parts of the response,
then use Rest.Request
, and simply read the file data from the response’s body.
Build and run the new test. It should pass, but it might take a little more time to complete since it makes two requests.
9. Creating Workflows
The TestDogApiImage
test from the previous step is longer than all the other tests.
Not only does it call two requests, but it calls two interlocking requests.
The response from the first request becomes part of the second request.
Request sequences like this are common in both applications and test automation.
Many workflows require chains of CRUD operations.
Workflows should be written as Screenplay interactions. Rather than calling a series of REST requests, an Actor could call just one interaction to perform the workflow. For example, when fetching random dog images, the caller probably doesn’t care what endpoints need to be called - they just want the picture of the dog!
Create a new class in the Interactions
directory named RandomDogImage.cs
,
and add the follow code to it:
using Boa.Constrictor.RestSharp;
using Boa.Constrictor.Screenplay;
using RestSharp;
using System;
using System.IO;
namespace Boa.Constrictor.Example
{
public class RandomDogImage : IQuestion<byte[]>
{
private RandomDogImage() { }
public static RandomDogImage FromDogApi() =>
new RandomDogImage();
public byte[] RequestAs(IActor actor)
{
var request = DogRequests.GetRandomDog();
var response = actor.Calls(Rest<CallDogApi>.Request<DogResponse>(request));
var resource = new Uri(response.Data.Message).AbsolutePath;
var imageRequest = new RestRequest(resource);
var extension = Path.GetExtension(resource);
var imageData = actor.Calls(DogImagesApi.Download(imageRequest, extension));
return imageData;
}
}
}
RandomDogImage
is a Question that gets a random dog image from Dog API.
Most of its code comes directly from the TestDogApiImage
test.
Now, refactor the TestDogApiImage
test to use RandomDogImage
:
[Test]
public void TestDogApiImage()
{
var imageData = Actor.AsksFor(RandomDogImage.FromDogApi());
imageData.Should().NotBeNullOrEmpty();
}
Read the first line in plain English: “The actor asks for a random dog image from Dog API.” Concise, descriptive calls like this make the Screenplay Pattern great. Screenplay’s syntax puts focus on intent, not mechanics. What ultimately matters is that the Actor gets a picture of a dog. Screenplay interactions can compose low-level actions like REST API calls into reusable workflows.
Web UI + REST API:
Screenplay interactions can compose Web UI and REST API interactions together.
For example, a Login
Task could authenticate a user via a REST API,
add the authentication token to a browser,
and refresh the browser to become logged in as that user.
Run the refactored test to make sure it still passes.
10. Simplifying REST Syntax
Let’s take a closer look at the Screenplay calls from the new RandomDogImage
Question:
actor.Calls(Rest<CallDogApi>.Request<DogResponse>(request))
actor.Calls(Rest<CallDogImagesApi>.Download(imageRequest))
These calls work, but the type generics on the Rest
class look out of place.
Try to read the first line in plain English:
“The actor calls REST call Dog API request dog response request.”
What? That doesn’t make sense.
We know what these calls will do, but the syntax is not very fluent.
As stated previously, the Rest
class is merely syntactic Screenplay sugar.
It provides a cleaner way to make REST requests using RestSharp.
Rest
works great for the “default” CallRestApi
Ability,
but it doesn’t work well for custom RestSharp Abilities.
The simplest way to improve the syntax is to use C#’s
using alias directives,
also known as “type aliases”.
Add the following lines to the top of RandomDogImage.cs
, immediately beneath the using
statements:
using DogApi = Boa.Constrictor.RestSharp.Rest<Boa.Constrictor.Example.CallDogApi>;
using DogImagesApi = Boa.Constrictor.RestSharp.Rest<Boa.Constrictor.Example.CallDogImagesApi>;
Then, rewrite the Screenplay calls:
// Old calls using the type generics
actor.Calls(Rest<CallDogApi>.Request<DogResponse>(request))
actor.Calls(Rest<CallDogImagesApi>.Download(imageRequest))
// New calls using the aliases
actor.Calls(DogApi.Request<DogResponse>(request))
actor.Calls(DogImagesApi.Download(imageRequest))
Read the new calls in plain English:
- “The actor calls Dog API to request a dog response from request.”
- “The actor calls Dog Images API to download an image request.”
These calls are much more readable now.
The full code for RandomDogImage.cs
should now look like this:
using Boa.Constrictor.Screenplay;
using RestSharp;
using System;
using System.IO.
using DogApi = Boa.Constrictor.RestSharp.Rest<Boa.Constrictor.Example.CallDogApi>;
using DogImagesApi = Boa.Constrictor.RestSharp.Rest<Boa.Constrictor.Example.CallDogImagesApi>;
namespace Boa.Constrictor.Example
{
public class RandomDogImage : IQuestion<byte[]>
{
private RandomDogImage() { }
public static RandomDogImage FromDogApi() =>
new RandomDogImage();
public byte[] RequestAs(IActor actor)
{
var request = DogRequests.GetRandomDog();
var response = actor.Calls(DogApi.Request<DogResponse>(request));
var resource = new Uri(response.Data.Message).AbsolutePath;
var imageRequest = new RestRequest(resource);
var extension = Path.GetExtension(resource);
var imageData = actor.Calls(DogImagesApi.Download(imageRequest, extension));
return imageData;
}
}
}
Rebuild and rerun the tests. They should all pass.
Aliases are Not Required: You do not need to create type aliases for RestSharp Abilities. Type aliases are optional. They simply improve code readability.
Limited Scope:
Unfortunately, using
aliases are local to the file in which they are declared.
They cannot be given global scope.
You will need to define aliases in each file that uses them.
11. Dumping Responses
So far in this tutorial, Boa Constrictor has handled responses in memory. It can also dump requests and downloads to files so that testers can review them later. Request dumps include the full request and response, field by field. Download dumps are the actual files downloaded in response bodies, like PDFs or PNGs. Dumps plainly show problems like error messages, bad status codes, and corrupted files.
Dumping is Optional: Dumping requests and downloads is optional. You do not need to dump files when using Boa Constrictor. Dumping does not happen by default - you must explicitly enable it.
The basic CallRestApi
Ability provides extra builder methods to configure dumping.
Each file type must be given a destination directory path and a filename prefix.
The example call below shows how to configure the Ability for dumping:
Actor.Can(
CallRestApi.At("https://dog.ceo/")
.DumpingRequestsTo("/path/to/dump/requests/", "DogApiRequest")
.DumpingDownloadsTo("/path/to/dump/downloads/", "DogApiImage"))
With this configuration, the Rest.Request
and Request.Download
methods will automatically dump their responses to these directories.
Boa Constrictor will create these directories if they do not already exist.
The dumped files will be named "<prefix>_<timestamp>.<extension>"
.
Request are dumped as JSON files, while downloads are dumped using their given file extension.
For example, a dog request dump file could be named "DogApiRequest_202104061352038197.json"
.
Dumping Naming Conventions:
Automation can make several requests and downloads during one execution.
Boa Constrictor uses a prefix like "DogApiRequest"
to categorize all responses from one Ability (meaning one base URL).
It uses timestamps to give a chronological sequence to dumped files.
Custom Abilities like CallDogApi
and CallDogImagesApi
do not inherit these dumping methods.
They need to specify dumping on their own.
They can either copy CallRestApi
’s builder methods for dumping, or they can customize how dumping is handled.
Let’s update CallDogApi
to automatically dump requests for Dog API.
Since Dog API does not provide the images to download, it does not need to be configured for dumping downloaded files.
Change CallDogApi.cs
to the following code:
using Boa.Constrictor.RestSharp;
using RestSharp;
using System.IO;
namespace Boa.Constrictor.Example
{
public class CallDogApi : AbstractRestSharpAbility
{
public const string BaseUrl = "https://dog.ceo/";
public const string RequestToken = "DogApiRequest";
private CallDogApi(RestClient client, string dumpDir) :
base(client)
{
RequestDumper = new RequestDumper(
"Dog API Request Dumper",
Path.Combine(dumpDir, RequestToken),
RequestToken);
}
public static CallDogApi DumpingTo(string dumpDir) =>
new CallDogApi(new RestClient(BaseUrl), dumpDir);
}
}
Let’s break down the changes:
Change | Reason |
---|---|
using System.IO; |
Required by the Path class. |
RequestToken |
A string constant for the request dump filename prefix. |
CallDogApi constructor |
The constructor now has a dumpDir argument for the path to the dump directory. |
RequestDumper |
The AbstractRestSharpAbility property for the request dumper. |
new RequestDumper(...) |
The class used for dumping requests. Needs a name, a dumping directory path, and a dump filename prefix. |
Path.Combine(dumpDir, RequestToken) |
Adds a subdirectory to the dumping directory for dumping Dog API requests. |
DumpingTo |
Builder method that replaces UsingBaseUrl . Takes in the dumping directory path. |
Let’s make a similar update to CallDogImagesApi
.
This Ability should dump both requests and downloads.
Change CallDogImagesApi.cs
to the following code:
using Boa.Constrictor.RestSharp;
using Boa.Constrictor.Screenplay;
using RestSharp;
using System.IO;
namespace Boa.Constrictor.Example
{
public class CallDogImagesApi : AbstractRestSharpAbility
{
public const string BaseUrl = "https://images.dog.ceo/";
public const string DownloadToken = "DogImagesApiDownload";
public const string RequestToken = "DogImagesApiRequest";
private CallDogImagesApi(RestClient client, string dumpDir) :
base(client)
{
RequestDumper = new RequestDumper(
"Dog Images API Request Dumper",
Path.Combine(dumpDir, RequestToken),
RequestToken);
DownloadDumper = new ByteDumper(
"Dog Images API Download Dumper",
Path.Combine(dumpDir, DownloadToken),
DownloadToken);
}
public static CallDogImagesApi DumpingTo(string dumpDir) =>
new CallDogImagesApi(new RestClient(BaseUrl), dumpDir);
}
}
These changes are very similar to the ones made for CallDogApi
,
but CallDogImagesApi
sets DownloadDumper
to a new ByteDumper
object.
The dumping subdirectories are different to keep files organized.
To use the updated Abilities, update ScreenplayRestApiAdvancedTest
.
Add the following import statement:
using System.IO;
And edit the [Setup]
method:
[SetUp]
public void InitializeScreenplay()
{
Actor = new Actor(name: "Andy", logger: new ConsoleLogger());
AssemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
Actor.Can(CallDogApi.DumpingTo(AssemblyDir));
Actor.Can(CallDogImagesApi.DumpingTo(AssemblyDir));
}
Both custom Abilities set the dumping directory to AssemblyDir
,
which is the directory where the Boa.Constrictor.Example
is located.
They can share the same dumping directory because each Ability adds subdirectories for different file types.
Assembly Directory: The assembly directory is typically the build output directory. It is a decent location for dumping files when running tests on a local machine. However, you should consider using a better output path when running tests in a Continuous Integration system.
Rerun the tests, and make sure they all pass.
Then, check the assembly directory for the dumped files.
(The assembly file is most likely located at
boa-constrictor\Boa.Constrictor.Example\bin\Debug\net7.0
.)
It should contain the following directories:
Assembly Directory
│
├── DogApiRequest
├── DogImagesApiDownload
└── DogImagesApiRequest
Request dumps can be large. Below is a snippet from one of the dumps for a Dog API request:
{
"Duration": {
"StartTime": "2021-04-06T16:54:25.0631404Z",
"EndTime": "2021-04-06T16:54:33.5413263Z",
"Duration": "00:00:08.4781859"
},
"Request": {
"Method": "GET",
"Uri": "https://dog.ceo/api/breeds/image/random",
"Resource": "api/breeds/image/random",
"Parameters": [],
"Body": null
},
"Response": {
"Uri": "https://dog.ceo/api/breeds/image/random",
"StatusCode": 200,
"ErrorMessage": null,
"Content": "{\"message\":\"https:\\/\\/images.dog.ceo\\/breeds\\/brabancon\\/n02112706_2087.jpg\",\"status\":\"success\"}",
"Headers": [
{
"Name": "Date",
"Value": "Tue, 06 Apr 2021 16:54:32 GMT",
"Type": "HttpHeader"
},
{
"Name": "Transfer-Encoding",
"Value": "chunked",
"Type": "HttpHeader"
},
// ...
],
// ...
}
}
And below is an image of a dog downloaded from Dog Images API:
File Storage: File dumps can fill up storage space on the file system. Remember to delete old dumps or archive them from time to time.
Conclusion
Congrats on finishing Part 3 of the tutorial! The example project should now be structured like this:
Boa.Constrictor.Example
│
├── Abilities
│ ├── CallDogApi.cs
│ └── CallDogImagesApi.cs
│
├── Interactions
│ ├── RandomDogImage.cs
│ └── SearchWikipedia.cs
│
├── Pages
│ ├── ArticlePage.cs
│ └── MainPage.cs
│
├── Requests
│ └── DogRequests.cs
│
├── Responses
│ └── DogResponse.cs
│
└── Tests
├── ScreenplayRestApiAdvancedTest.cs
├── ScreenplayRestApiBasicTest.cs
└── ScreenplayWebUiTest.cs
Now that you have completed the tutorial, you should be ready to use Boa Constrictor in your own projects!