Enhance your .NET Testing #8: Contract tests with Verify

Pierre Belin
Pierre Belin
Enhance your .NET Testing #8: Contract tests with Verify
Table of Contents
Table of Contents

The most common way to test the values of an object is to use assertions for each property to check exactly what the object contains.

This can be done for types, classes, enumerables. The more aspects of the object that are tested, the more certain you can be that everything works as expected.

If you didn't read the previous article, I recommend you to do it:

Enhance your .NET Testing #7: 5 best practices to write better tests
In the digital age, where software permeates every facet of life, the significance of reliable code cannot be overstated. Imagine the chaos when a banking system falters, or the disruption when an e-commerce platform crashes. These scenarios underscore the critical role of testing in the software development lifecycle. Testing is

Assertions can be made quickly with small classes. It gets really annoying when your class contains a class, which contains a class... and so on. Nesting creates a huge amount of validations.

The complexity can be solved by splitting the current class into minor classes to avoid manipulation, and by reducing the scope of each method, but this is not always possible, mainly when we want to validate an end-to-end object update workflow.

In this case, we want to be sure that the resulting object is exactly as expected, as in API response or data ingestion updates. Let's start with an assert validation of the following code. It creates a goat with a name and a color, then adds a habit.

public Task Test_Verify()
{
    var goat = new Goat
    {
        Name = "Bicky",
        Color = "Grey"
    };
    goat.AddHabit(".NET", 3); // First param is Name and second is Difficulty

    Assert.Equal("Bicky", goat.Name);
    Assert.Equal("Grey", goat.Color);

    Assert.NotNull(goat.Habits);
    Assert.Equal(1, goat.Habits.Count);
    Assert.Equal(".NET", goat.Habits.First().Name);
    Assert.Equal(3, goat.Habits.First().Difficulty);
}

As you can see, it is quite redundant and not really interesting to do with only 3 properties included in a list.

The job is done, all properties are properly tested.

If you pay attention, something is not tested here! Have you noticed what it is?

Imagine that this class has been working fine for 2 years, the team of developers has changed (or you may still be on this project but you have to do other things in this period) and the Product Owner asks you to add the property Age on the goats.

The first thing you do is update the Goat class, add the implementation requirements, create tests to validate and push to the main branch.

There's still something unpleasant here.

You remember our test above. It's still green, when it shouldn't be. We only tested the properties we knew about when the test was created, and the code won't raise any red code since the assertions are still true.

Imagine exposing unwanted properties on your API without noticing it...

Verify to save us all

Presentation

Verify, a .NET library, helps us overcome this problem by generating a JSON file modeling the asserted object.

It works on all C# test libraries: xUnit, MSTest, NUnit and Expector for F#.

GitHub - VerifyTests/Verify: Verify is a snapshot tool that simplifies the assertion of complex data models and documents.
Verify is a snapshot tool that simplifies the assertion of complex data models and documents. - VerifyTests/Verify

Each test using Verify will generate a file containing the test result. This file will be compared to the verified file of the test with the diff comparison.

If the two files are identical, the tested method returns the expected result. If not, something has changed and an error is raised. It is very simple!

The only difference is the first run because there is no file to compare with. The test will be red, and you will have to merge the received file into the verified file which is currently empty.

The repository contains an excellent diagram of how this works.

We will take a simple example to show how Verify works. It is possible to go much further by using the extensions :

  • Ensure the completeness of an API response (code, content, headers and cookies)
  • Validate the Moq configuration inside the test file to notice any changes of the initial state
  • Track EF changes and SQL query creation
  • Check the log flow

Quick example

The test has been updated to remove the assertions and replace them with the Verify() function.

The goat argument will be the serialized and compared result.

public Task Test_Verify()
{
    var goat = new Goat
    {
        Name = "Bicky",
        Color = "Grey"
    };
    goat.AddHabit(".NET", 3);
    
    return Verify(goat);
}

Once the test launched, everything is red. 2 files are also creating, one for the verification and one containing the received object.

A pop-up should open to show the difference between the two files. As expected, the verified file is empty. Merge the contents to have the first verified file completed.

If you run the test again, it should be green.

Add extra settings

Verify can be parameterized to modify the verification operation using the VerifySettings class.

To understand why, let's take a few examples.

The first, which can be annoying, is to disable the popup that opens every time a comparison fails, using the DisableDiff() option.

The second, for the sake of organizing files, is to define the folder in which verified files are stored with the UseDirectory() option. It's rather annoying to have a mixture of test class files and verified results. For simplicity's sake, we recommend creating a sub-folder with results only.

The third problem is when the content of the result contains a random part such as a date or a GUID generated when an element is created (which is often the case in an API). Verify's built-in solution is to use AddScrubber() to modify the content of the result, either with constants or regexes.

_settings.AddScrubber(builder => {    
	var modifiedContent = Regex.Replace(builder.ToString(), "MYREGEX", "MYREPLACEMENT");    
    builder.Clear();    
    builder.Append(modifiedContent);
}); 

Use Verify for contracts tests

Now that Verify's configuration is out of the way, it's time to put it to practical use: contract testing.

Contract testing is useful when setting up an exposure, whether HTTP or something like GraphQL, to ensure that the system doesn't regress from one version to the next. This is all the more important on public APIs, where other players wired into our system expect it not to change from one version to the next.

To ensure this, it's important to check 2 elements: input requests and output responses.

One way of making sure that this is the case is to use a WebApplicationFactory to load the entire application and start a HttpClient. If you're unfamiliar with this, I suggest you read this article before continuing:

Enhance your .NET Testing #1: WebApplicationFactory
The WebApplicationFactory class allows you to create a factory for bootstrapping an application in memory for testing.

The test is divided into several parts: configuring Verify, sending the HTTP request and retrieving the response to be validated.

[Fact]
public async Task ShouldXXX()
{
	// Configure Verify to replace Bearer
    var settings = new VerifySettings();
    settings.AddScrubber(builder => {
        var pattern = @"Bearer\s[^\s]+";
        var replacement = "Bearer REPLACED_BEARER_TOKEN";
        var modifiedContent = Regex.Replace(builder.ToString(), pattern, replacement);
        builder.Clear();
        builder.Append(modifiedContent);
    });
            
    // Configure HttpClient to run again the API or the WebApplicationFactory
    var client = new HttpClient(); 
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "MYBEARER");
    var uri = new Uri(client.BaseAddress, "/API/ROUTE/?param=test");
    
    // Send request
    var response = await client.GetAsync(uri);
    
    // Extract and verify contract from request
    var contractContent = await ExtractContent(response);
    await Verify(contractContent);
}

The HttpResponseMessage contains all the request information, both from the request and the response. All you need to do is extract and serialize it, so that Verify can use it.

Very important point: the contents of both the request and the response must be retrieved with ReadAsStringAsync to be sure of having the raw version of the contents. If you use ReadAsJsonAsync or a serializer, you run the risk of modifying the raw result, and therefore of having a mismatch between what you're testing and the real return.

protected virtual async Task<string> ExtractContent(HttpResponseMessage response)
{
    var requestContent = response.RequestMessage?.Content is null ? "" : await response.RequestMessage.Content!.ReadAsStringAsync();
    
    var content = new { 
        Request = new {
            Headers = response.RequestMessage.Headers,
            Method = response.RequestMessage.Method,
            RequestUri = response.RequestMessage.RequestUri!.PathAndQuery,
            Version = response.RequestMessage.Version,
            Content = requestContent
        },
        Response = new {
            Headers = response.Headers,
            StatusCode = response.StatusCode,
            Content = await response.Content.ReadAsStringAsync()
        }
    };
    return JsonSerializer.Serialize(content, new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping });
}

This gives the following result:

{
  "Request": {
    "Headers": [
      {
        "Key": "Authorization",
        "Value": [
          "Bearer REPLACED_BEARER_TOKEN
        ]
      }
    ],
    "Method": {
      "Method": "GET"
    },
    "RequestUri": "/API/ROUTE/?param=test",
    "Version": "1.1",
    "Content": ""
  },
  "Response": {
    "Headers": [],
    "StatusCode": 200,
    "Content": "{\"fieldA\":[],\"fieldB\":1,\"fieldC\":null}"
  }
}

We are now certain that the /API/ROUTE/?param=test route call in GET must always contain a bearer and must always have an HTTP 200 code with contents equal to "{"fieldA":[], "fieldB":1, "fieldC":null}".

If one of these elements changes (case included), the test will detect it.

Conclusion

Verify is a powerful library for simplifying the assertion of complex data models and documents such as API responses or log/PDF files.

Beware, Verify does not replace all property assertions. Personally, I see a huge use for API, logs and full object manipulation, but this is only a small part of my validation testing.

Last but not least, all *.received.* files must be excluded from source control if you want your git commits to stay clean. If you want to delete them recursively with a PowerShell script, you can use the following command :

Get-ChildItem -Path . -Recurse -File | Where-Object { $_.Name -like "*.received.*" } | Remove-Item

To go further:

GitHub - VerifyTests/Verify: Verify is a snapshot tool that simplifies the assertion of complex data models and documents.
Verify is a snapshot tool that simplifies the assertion of complex data models and documents. - VerifyTests/Verify

Have a goat day 🐐



Join the conversation.

Great! Check your inbox and click the link
Great! Next, complete checkout for full access to Goat Review
Welcome back! You've successfully signed in
You've successfully subscribed to Goat Review
Success! Your account is fully activated, you now have access to all content
Success! Your billing info has been updated
Your billing was not updated