In this post, we’re going to cover the recent extensions to the DalSoft RestClient library which allows you to easily test a REST API using C#.
This post is aimed at anyone (devs, testers, etc) who tests REST APIs and is suitable for all levels. If you haven’t written a test before though we recommend reading this unit test series. The main focus point of this article will be how to perform system testing on your REST API.
If you haven’t used DalSoft RestClient before you can learn about it by reading this post.
The code examples can be found in this GitHub repository.
Let’s get into it.
About System Testing
There are four types of testing: system, integration, unit, and acceptance testing.
The type of testing we’re going to cover in this post is System Testing. System Testing is a form of “Black Box” testing which means testing without knowing anything about the internals of the System Under Test (SUT). It’s usually the last gate before deploying changes into production.
System Testing is usually done on an environment that is as close to production as possible such as a staging or UAT (User Acceptance Testing) environment. Some people even system test using their production environment as part of a continual QA process.
For our example, we are going to test the JSONPlaceholder API.
Let’s start by installing the DalSoft RestClient Package first.
Install the DalSoft RestClient Package
There are two easy ways to do this:
Install via .NET CLI:
dotnet add package DalSoft.RestClient
Or Install via NuGet through the Package Management Console:
Install-Package DalSoft.RestClient
That is all we need in terms of the required libraries.
Optionally Create an “SDK” to Test your REST API
For our example, we are going to create an “SDK” to system test the JSONPlaceholder API. This is optional because we could just use Rest Client’s dynamic features instead.
To create the SDK we are going to make use of Rest Client’s Resource method. To do this all we need to do is create a Resource
class. A Resource
class has properties and methods returning strings that represent our resources. We then can use the methods and properties in the Resource
method as an expression.
Here is an example of our simple SDK:
public class UserResource { public string GetUsers() => "users"; public string GetUser(int userId) => $"{GetUsers()}/{userId}"; public string GetUserPosts(int userId) => $"{GetUser(userId)}/posts"; }
Use Verify in our System Tests
Now that we have a Resource class, all we need to do to call the JSONPlaceholder API is pass it as a generic parameter to the Resource
method along with an expression parameter representing the Resource, and finally, call the HTTP method we want.
This sounds harder than it is and it can all be wrapped up easily in a couple of lines of code:
var client = new RestClient("https://jsonplaceholder.typicode.com", new Config() .SetJsonSerializerSettings(new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); // Get all users from jsonplaceholder API var response = await client.Resource<UserResource>(api => api.GetUsers()).Get();
Now we know we can call the JSONPlaceholder API easily it’s time to write some system tests.
This is easily done using the Verify
method.
To use the Verify
method you provide a generic parameter to cast your response to (usually either your Model, a string or HttpResponseMessage
) and an expression returning a boolean representing the condition you want to verify.
Let’s write some tests.
Verify that the resource /users/1
returns a 200 OK
status code:
[Fact] public async Task Get_UserWithId1_ReturnsOkStatusCode() { var client = new RestClient("https://jsonplaceholder.typicode.com", new Config() .SetJsonSerializerSettings(new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); await client .Resource<UserResource>(api => api.GetUser(1)).Get() .Verify<HttpResponseMessage>(response => response.StatusCode == HttpStatusCode.OK); }
Verify that the response body of the JSON returned is what we were expecting:
[Fact] public async Task Get_UserWithId1_ReturnsJsonStringWithUsernameBret() { var client = new RestClient("https://jsonplaceholder.typicode.com", new Config() .SetJsonSerializerSettings(new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); await client .Resource<UserResource>(api => api.GetUser(1)).Get() .Verify<string>(s => s.Contains("\"username\": \"Bret\"")); }
We can do one better. We can verify that the response body can be cast to a User object, and the User object returned is what we expect:
[Fact] public async Task Get_UserWithId1_ReturnsUserModelWithUsernameBret() { var client = new RestClient("https://jsonplaceholder.typicode.com", new Config() .SetJsonSerializerSettings(new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); await client .Resource<UserResource>(api => api.GetUser(1)).Get() .Verify<User>(user => user.Username == "Bret"); }
You can also chain the Verify methods together to be nice and “Fluent”:
[Fact] public async Task Get_UserWithId1_ReturnsUserWithUsernameBretAndOkStatusCode() { var client = new RestClient("https://jsonplaceholder.typicode.com", new Config() .SetJsonSerializerSettings(new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); await client .Resource<UserResource>(api => api.GetUser(1)).Get() .Verify<User>(user => user.Username == "Bret") .Verify<HttpResponseMessage>(response => response.StatusCode == HttpStatusCode.OK); }
What Happens if a Verify Test Condition Fails?
So what happens when a Verify test condition fails?
Simple AggregateException
is thrown and the InnerExceptions
contain a VerifiedFailed
exception for each failure because an exception is thrown our test fails. The VerifiedFailed
exception contains a descriptive message of exactly what condition failed.
Let’s make our test fail and see what it looks like. Take the last test we did and make it fail:
[Fact] public async Task Get_UserWithId1_ReturnsUserWithUsernameBretAndOkStatusCode() { var client = new RestClient("https://jsonplaceholder.typicode.com", new Config() .SetJsonSerializerSettings(new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); await client .Resource<UserResource>(api => api.GetUser(1)).Get() .Verify<User>(user => user.Username == "This will Fail") .Verify<HttpResponseMessage>(response => response.StatusCode == HttpStatusCode.BadRequest); }
Running this test will give you the following output:
System.AggregateException: One or more errors occurred.
(user => (user.Username == "This will Fail") was not verified)
(response => (Convert(response.StatusCode, Int32) == 400) was not verified)
There are more advanced features for handling Verify failures that we will cover later on in the post.
Use DalSoft Rest Client’s Dynamic Features
We mentioned creating the Resource class or “SDK” was optional. This is because if you don’t pass the generic parameter a dynamic object is returned, which you can work with right away!
Here’s how it looks, you can start testing a REST API right away with a few lines of code:
[Fact] public async Task Get_UserWithId1_ReturnsDynamicWithUsernameBretAndOkStatusCode() { var client = new RestClient("https://jsonplaceholder.typicode.com", new Config() .SetJsonSerializerSettings(new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); await client .Resource("users/1").Get() .Verify(userIsBret => userIsBret.username == "Bret") .Verify(httpResponseMessageIsOk => httpResponseMessageIsOk.HttpResponseMessage.StatusCode == HttpStatusCode.OK); }
You may have noticed the strange variable naming in this test, this is because when using a dynamic object you can’t get the full expression in the VerifyFailed
exception message. Instead, we only get the parameter name, so naming it like this tells us what failed. For example, if the test above failed it would look like:
System.AggregateException: One or more errors occurred.
(dynamic userIsBret => ... was not verified)
(dynamic httpResponseMessageIsOk => ... was not verified)
You can also mix and match Generic and Dynamic Verify
methods:
[Fact] public async Task Get_UserWithId1_ReturnsDynamicWithUsernameBretAndOkStatusCode() { var client = new RestClient("https://jsonplaceholder.typicode.com", new Config() .SetJsonSerializerSettings(new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); await client .Resource("users/1").Get() .Verify(userIsBret => userIsBret.username == "Bret") .Verify(httpResponseMessageIsOk => httpResponseMessageIsOk.HttpResponseMessage.StatusCode == HttpStatusCode.OK) .Verify<HttpResponseMessage>(response => response.StatusCode == HttpStatusCode.OK); }
Fine-Grained Control Using OnVerifyFailed
We’ve covered what happens if a Verify test condition fails earlier and for most testing scenarios this should work fine.
There are advanced options to handle failed verifications, but it’s worth noting these are designed more for validation than testing. For advanced scenarios, we can use the OnVerifyFailed
callback to handle failed verifications. By default instead of throwing the OnVerifyFailed
callback just passes an AggregateException
(containing VerifiedFailed
exceptions) and a response object to an Action you provide.
Here’s what it looks like:
[Fact] public async Task Get_UserWithId1Example_TestingUsingOnVerifyFailed() { var client = new RestClient("https://jsonplaceholder.typicode.com", new Config() .SetJsonSerializerSettings(new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); await client .Resource<UserResource>(api => api.GetUser(1)).Get() .Verify<User>(user => user.Username == "Fail") .Verify<HttpResponseMessage>(response => response.StatusCode == HttpStatusCode.BadRequest) .OnVerifyFailed((aggregateException, response) => { // Handle the failure here }); }
Running the above code your notice that the OnVerifyFailed
callback is called and no exceptions are thrown.
Like Verify
you can use a generic parameter to cast the response:
[Fact] public async Task Get_UserWithId1Example_TestingUsingOnVerifyFailed() { var client = new RestClient("https://jsonplaceholder.typicode.com", new Config() .SetJsonSerializerSettings(new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); await client .Resource<UserResource>(api => api.GetUser(1)).Get() .Verify<User>(user => user.Username == "Fail") .Verify<HttpResponseMessage>(response => response.StatusCode == HttpStatusCode.BadRequest) .OnVerifyFailed<HttpResponseMessage>((aggregateException, response) => { if (response.StatusCode == HttpStatusCode.BadRequest) // ... else // ... }); }
You can use the OnVerifyFailed
callback to handle and throw by setting the throwOnVerifyFailed
parameter to true:
[Fact] public async Task Get_UserWithId1Example_TestingUsingOnVerifyFailed() { var client = new RestClient("https://jsonplaceholder.typicode.com", new Config() .SetJsonSerializerSettings(new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); await client .Resource<UserResource>(api => api.GetUser(1)).Get() .Verify<User>(user => user.Username == "Fail") .Verify<HttpResponseMessage>(response => response.StatusCode == HttpStatusCode.BadRequest) .OnVerifyFailed((aggregateException, response) => { // Handle the failure here }, throwOnVerifyFailed: true); }
Running the above test will cause OnVerifyFailed
to be called and an exception to be thrown.
Where in the chain OnVerifyFailed
appears is important. OnVerifyFailed
will only handle Verifications above itself in the chain. By default (throwOnVerifyFailed: false
) only the first OnVerifyFailed
callback will “handle” the failures, and subsequent OnVerifyFailed
callbacks in the chain won’t be called.
However, we can use the throwOnVerifyFailed
parameter to change this behavior.
[Fact] public async Task Get_UserWithId1Example_TestingUsingOnVerifyFailed() { var client = new RestClient("https://jsonplaceholder.typicode.com", new Config() .SetJsonSerializerSettings(new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); await client .Resource<UserResource>(api => api.GetUser(1)).Get() .Verify<User>(user => user.Username == "Fail") .Verify<HttpResponseMessage>(response => response.StatusCode == HttpStatusCode.BadRequest) .OnVerifyFailed((aggregateException, response) => { // I will be called on Verify failures }) .OnVerifyFailed((aggregateException, response) => { // I will NOT be called on Verify failures }); }
In the example below the last invoked OnVerifyFailed
callback in the chain decides whether an exception should be thrown. This simply means if the last OnVerifyFailed
callback in the chain has Verify failures and throwOnVerifyFailed
is true an exception is thrown.
[Fact] public async Task Get_UserWithId1Example_TestingUsingOnVerifyFailed() { var client = new RestClient("https://jsonplaceholder.typicode.com", new Config() .SetJsonSerializerSettings(new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); await client .Resource<UserResource>(api => api.GetUser(1)).Get() .Verify<User>(user => user.Username == "Fail") .Verify<HttpResponseMessage>(response => response.StatusCode == HttpStatusCode.BadRequest) .OnVerifyFailed((aggregateException, response) => { // I will be called on Verify failures }, throwOnVerifyFailed:true) // Throw exceptions to next handler .OnVerifyFailed((aggregateException, response) => { // I WILL be called on Verify failures and will throw an exception }, throwOnVerifyFailed:true); }
Putting it All Together
Putting all this together gives us fine-grained control over what to do with the Verify failures.
This contrived example shows how to handle individual groups of Verify failures:
[Fact(DisplayName = "This test is meant to fail")] public async Task Get_UserWithId1Example_TestingUsingOnVerifyFailed() { var client = new RestClient("https://jsonplaceholder.typicode.com", new Config() .SetJsonSerializerSettings(new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); await client .Resource<UserResource>(api => api.GetUser(1)).Get() .Verify<User>(user => user.Username == "Fail") .OnVerifyFailed<HttpResponseMessage>((aggregateException, response) => { // Callback invoked if the Verify above fails Assert.Equal(1, aggregateException.InnerExceptions.Count); }, throwOnVerifyFailed: false) .Verify<HttpResponseMessage>(response => response.StatusCode == HttpStatusCode.BadRequest) .OnVerifyFailed<HttpResponseMessage>((aggregateException, response) => { // Because the previous OnVerifyFailed set throwOnVerifyFailed to false callback only invoked if above Verify failure Assert.Equal(1, aggregateException.InnerExceptions.Count); }, throwOnVerifyFailed: true) .Verify<User>(response => response.Username == "Peter") .OnVerifyFailed<User>((aggregateException, response) => { // Because the previous OnVerifyFailed set throwOnVerifyFailed to true callback invoked if the above Verify and previous Verify fails // aggregateException contains above Verify and previous Verify failures Assert.Equal(2, aggregateException.InnerExceptions.Count); }, throwOnVerifyFailed: true); // Last OnVerifyFailed set throwOnVerifyFailed to true so an exception will be thrown // this will contain both the above and previous Verify failures, but not the first failure as throwOnVerifyFailed was set to false }
The first OnVerifyFailed
callback only handles the Verify failure directly above it in the chain. The next OnVerifyFailed
callback in the chain only handles the Verify
failure directly above but sets throwOnVerifyFailed
to true. This means the last OnVerifyFailed
is called for the Verify
failure directly above as well as the previous failure. An exception is thrown because the last OnVerifyFailed
callback has throwOnVerifyFailed
set to true.
Use Rest Client Factory in Your Test Fixture
Creating HttpClient
(which Rest Client extends) is expensive. In most cases for system tests, you could use a Static field with a one-time initialized Rest Client. However, it would be better to use HttpClientFactory
/ RestClientFactory
, in a future post, we will explore how to use a Test Fixture with RestClientFactory
.
Conclusion
In this post, you’ve learned how to use DalSoft Rest Client to system test a REST API. We built a simple SDK and learned how easy it is to call a REST API right away using Rest Client’s dynamic features.
All the source code for this post is in this GitHub repository.