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.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

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);
}

View live example

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);
}

View live example

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
        });
}

View live example

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 
}

View live example

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.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!