FluentAssertions.Web 2.0.5

There is a newer version of this package available.
See the version list below for details.
dotnet add package FluentAssertions.Web --version 2.0.5
                    
NuGet\Install-Package FluentAssertions.Web -Version 2.0.5
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FluentAssertions.Web" Version="2.0.5" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="FluentAssertions.Web" Version="2.0.5" />
                    
Directory.Packages.props
<PackageReference Include="FluentAssertions.Web" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add FluentAssertions.Web --version 2.0.5
                    
#r "nuget: FluentAssertions.Web, 2.0.5"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package FluentAssertions.Web@2.0.5
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=FluentAssertions.Web&version=2.0.5
                    
Install as a Cake Addin
#tool nuget:?package=FluentAssertions.Web&version=2.0.5
                    
Install as a Cake Tool

FluentAssertions.Web

This is a FluentAssertions and AwesomeAssertions extension over the HttpResponseMessage object.

It provides assertions specific to HTTP responses and outputs rich errors messages when the tests fail, so less time with debugging is spent.

Assertions at a glance

response.Should().Be200Ok();                                               // status codes: Be200Ok, Be404NotFound, Be400BadRequest, Be5XXServerError, ...
response.Should().Be400BadRequest().And.HaveError("Author", "*required*"); // validation errors carried by a 400 response
response.Should().BeAs(new { Author = "John" });                           // the body is equivalent to an object
response.Should().Satisfy<IEnumerable<Comment>>(comments =>               // any assertions over the deserialized body
    comments.Should().HaveCount(2).And.OnlyHaveUniqueItems(c => c.CommentId));
response.Should().HaveHeader("X-Correlation-ID").And.Match("*-*");         // headers
response.Should().MatchInContent("*\"author\"*");                          // raw content

Jump straight to the examples, or to the full API listing.

A typical test:

[Fact]
public async Task Post_ReturnsOk()
{
    // Arrange
    var client = _factory.CreateClient();

    // Act
    var response = await client.PostAsync("/api/comments", new StringContent(
    """
    {
      "author": "John",
      "content": "Hey, you..."
    }
    """, Encoding.UTF8, "application/json"));

    // Assert
    response.Should().Be200Ok();
}

When a test fails, you see the whole conversation

Every failure message produced by this library is followed by the complete HTTP response and the originating HTTP request — status line, headers and both bodies, the same way an HTTP interceptor like Fiddler would render them. There is no need to attach a debugger or to watch response.Content.ReadAsStringAsync().Result anymore: the reason for the failure and the payload that caused it are already in the test output.

Running the test above with dotnet test against an endpoint that answers 201 Created instead of 200 OK prints:

  Failed Sample.Api.Tests.CommentsControllerTests.Post_ReturnsOk [614 ms]
  Error Message:
   Expected response to be HttpStatusCode.OK {value: 200}, but found HttpStatusCode.Created {value: 201}.

The HTTP response was:

HTTP/1.1 201 Created
Location: http://localhost/api/Comments/1
X-Correlation-ID: 42c3a077-12ec-470e-8569-ddeaade90127
Content-Type: application/json; charset=utf-8

{
  "author": "John",
  "content": "Hey, you...",
  "commentId": 1
}

The originating HTTP request was:

POST http://localhost/api/comments HTTP 1.1
Content-Type: application/json; charset=utf-8
Content-Length: 50
{
  "author": "John",
  "content": "Hey, you..."
}
  Stack Trace:
     at FluentAssertions.Execution.XUnit2TestFramework.Throw(String message)
   at FluentAssertions.Execution.TestFrameworkProvider.Throw(String message)
   at FluentAssertions.Execution.DefaultAssertionStrategy.HandleFailure(String message)
   at FluentAssertions.Web.HttpResponseMessageAssertions.Be200Ok(String because, Object[] becauseArgs)
   at Sample.Api.Tests.CommentsControllerTests.Post_ReturnsOk()
--- End of stack trace from previous location ---

The same text is shown in the Test Detail Summary of Visual Studio and Rider, and ends up in the CI logs, so the failure can usually be assessed without even running the test locally:

FailedTest1

A couple of details about that output:

  • when there is no originating request (for example because the HttpResponseMessage was created by hand), the last section reads The originating HTTP request was <null>.
  • when the response content is disposed it is reported as ***** Content is disposed so it cannot be read. *****
  • when the response content is bigger than ResponseFormatterOptions.MaximumReadableBytes (1.25 MB by default), it is truncated and a ***** Content is too large to display and only a part is printed. ***** warning is printed instead — see Response Formatting

Why?

Writing tests for ASP.NET Core APIs and or any APIs by using the HttpClient classes leads to some repetitive code and also often to an incomplete one. When the test fails, the developer needs to debug the test in order to assess the failure reason, as the test itself did not usually consider what happens in this case.

Thus this library solves two problems:

Once the response is ready you'll want to assert it. With first level properties like StatusCode is somehow easy, especially with FluentAssertions/AwesomeAssertions, but often we need more, like to deserialize the content into an object of a certain type and then to Assert it. Or to simply assert something about the response content itself. Soon duplication code occurs and the urge to reduce it is just the next logical step.

Debugging failed tests interrupts the programmer's flow state

When a test is failing, the following actions are taken most of the time:

  • attach the debugger to the line containing var response = await client..
  • debug the failing test
  • add an Watch for response.Content.ReadAsStringAsync().Result and see the actual response content

And this can be avoided, if the Test Detail Summary contains the request and the response information, providing a similar experience as with an HTTP interceptor like Fiddler. See When a test fails, you see the whole conversation for the exact output.

Status

Build status Quality Gate Status

NuGet NuGet FA v8

NuGet AwesomeAssertions

NuGet HttpMessageFormatter

Getting started

If you are using FluentAssertions < 8.0.0

dotnet add package FluentAssertions.Web

If you are using FluentAssertions >= 8.0.0

dotnet add package FluentAssertions.Web.v8

If you are using AwesomeAssertions >= 8.0.0

dotnet add package AwesomeAssertions.Web

FluentAssertions.Web Examples

  • Asserting that the response content of an HTTP POST request is equivalent to a certain object
[Fact]
public async Task Post_ReturnsOkAndWithContent()
{
    // Arrange
    var client = _factory.CreateClient();

    // Act
    var response = await client.PostAsync("/api/comments", new StringContent(
    """
    {
      "author": "John",
      "content": "Hey, you..."
    }
    """, Encoding.UTF8, "application/json"));

    // Assert
    response.Should().BeAs(new
    {
        Author = "John",
        Content = "Hey, you..."
    });
}

<details> <summary><code>dotnet test</code> output when this assertion fails <em>(here: the API persisted a different <code>content</code>)</em></summary>

  Failed Sample.Api.Tests.CommentsControllerTests.Post_ReturnsOkAndWithContent [41 ms]
  Error Message:
   Expected response to have a content equivalent to a model, but it has differences:

    - expected property response.Content to be "Expected, but not really there..." with a length of 33, but "Hey, you..." has a length of 11, differs near "Hey" (index 0).
. 

The HTTP response was:

HTTP/1.1 201 Created
Location: http://localhost/api/Comments/1
X-Correlation-ID: 4f1b4ccb-d4f2-4105-954c-f0a999ce8f27
Content-Type: application/json; charset=utf-8

{
  "author": "John",
  "content": "Hey, you...",
  "commentId": 1
}

The originating HTTP request was:

POST http://localhost/api/comments HTTP 1.1
Content-Type: application/json; charset=utf-8
Content-Length: 50
{
  "author": "John",
  "content": "Hey, you..."
}
  Stack Trace:
     ...

</details>

  • Asserting that the response is 200 OK and the content is like an array of specific objects:
[Fact]
public async Task Get_Returns_Ok_With_CommentsList()
{
    // Arrange
    var client = _factory.CreateClient();

    // Act
    var response = await client.GetAsync("/api/comments");

    // Assert
    response.Should().Be200Ok().And.BeAs(new[]
    {
        new { Author = "Adrian", Content = "Hey" },
        new { Author = "Johnny", Content = "Hey!" }
    });
}

<details> <summary><code>dotnet test</code> output when this assertion fails <em>(here: the response contained one comment more than expected)</em></summary>

  Failed Sample.Api.Tests.CommentsControllerTests.Get_Returns_Ok_With_CommentsList [403 ms]
  Error Message:
   Expected response to have a content equivalent to a model, but it has differences:

    - expected response to be a collection with 1 item(s), but {{ Author = Adrian, Content = Hey }, { Author = Johnny, Content = Hey! }}"
"contains 1 item(s) more than"
"{{ Author = Adrian, Content = Hey }}.
. 

The HTTP response was:

HTTP/1.1 200 OK
X-Correlation-ID: 7150524b-1b2f-4fc5-9574-8b797d776c95
Content-Type: application/json; charset=utf-8

[
  {
    "author": "Adrian",
    "content": "Hey",
    "commentId": 1
  },
  {
    "author": "Johnny",
    "content": "Hey!",
    "commentId": 2
  }
]

The originating HTTP request was:

GET http://localhost/api/comments HTTP 1.1
  Stack Trace:
     ...

</details>

  • Asserting that the response is an HTTP 400 BadRequest and contains a single error message
[Fact]
public async Task Post_WithNoAuthorButWithContent_ReturnsBadRequestWithAnErrorMessageRelatedToAuthorOnly()
{
    // Arrange
    var client = _factory.CreateClient();

    // Act
    var response = await client.PostAsync("/api/comments", new StringContent(
    """
    {
      "content": "Hey, you..."
    }
    """, Encoding.UTF8, "application/json"));

    // Assert
    response.Should().Be400BadRequest()
        .And.OnlyHaveError("Author", "The Author field is required.");
}

<details> <summary><code>dotnet test</code> output when this assertion fails <em>(here: the response reported an error for the <code>Content</code> field as well, so there is more than one)</em></summary>

  Failed Sample.Api.Tests.CommentsControllerTests.Post_WithNoAuthorButWithContent_ReturnsBadRequestWithAnErrorMessageRelatedToAuthorOnly [31 ms]
  Error Message:
   Expected response to only contain an error message related to the "Author" field, but more than this one was found.

The HTTP response was:

HTTP/1.1 400 BadRequest
X-Correlation-ID: 5b4b76b1-a1fe-4088-bbf1-5c14f82f4dc3
Content-Type: application/problem+json; charset=utf-8

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Author": [
      "The Author field is required."
    ],
    "Content": [
      "The Content field is required."
    ]
  },
  "traceId": "00-fd941763cb7e1f0fd046d1551ddddc92-6d3df58bb1d79a64-00"
}

The originating HTTP request was:

POST http://localhost/api/comments HTTP 1.1
Content-Type: application/json; charset=utf-8
Content-Length: 35
{
  "author": "",
  "content": ""
}
  Stack Trace:
     ...

</details>

  • Asserting the response content once deserialized into a strongly typed object it satisfies a certain assertion
[Fact]
public async Task Get_Returns_Ok_With_CommentsList_With_TwoUniqueComments()
{
    // Arrange
    var client = _factory.CreateClient();

    // Act
    var response = await client.GetAsync("/api/comments");

    // Assert
    response.Should().Satisfy<IEnumerable<Comment>>(model => 
            model.Should().HaveCount(2).And.OnlyHaveUniqueItems(c => c.CommentId));
}

<details> <summary><code>dotnet test</code> output when this assertion fails <em>(here: the collection had a different count)</em></summary>

  Failed Sample.Api.Tests.CommentsControllerTests.Get_Returns_Ok_With_CommentsList_With_TwoUniqueComments [12 ms]
  Error Message:
   Expected response to satisfy one or more model assertions, but it wasn't: 

    - expected model to contain 3 item(s), but found 2: {
    Sample.Api.Controllers.Comment
    {
        Author = "Adrian", 
        CommentId = 1, 
        Content = "Hey"
    }, 
    Sample.Api.Controllers.Comment
    {
        Author = "Johnny", 
        CommentId = 2, 
        Content = "Hey!"
    }
}
.


The HTTP response was:

HTTP/1.1 200 OK
X-Correlation-ID: 29da9489-0e1e-48b7-8f80-3e3a97bbc749
Content-Type: application/json; charset=utf-8

[
  {
    "author": "Adrian",
    "content": "Hey",
    "commentId": 1
  },
  {
    "author": "Johnny",
    "content": "Hey!",
    "commentId": 2
  }
]

The originating HTTP request was:

GET http://localhost/api/comments HTTP 1.1
  Stack Trace:
     ...

</details>

  • Asserting the response content once deserialized into a anonymous object it satisfies a certain assertion
[Fact]
public async Task Get_WithCommentId_Returns_A_NonSpam_Comment()
{
    // Arrange
    var client = _factory.CreateClient();

    // Act
    var response = await client.GetAsync("/api/comments/1");

    // Assert
    response.Should().Satisfy(givenModelStructure: new
    {
        Author = default(string),
        Content = default(string)
    }, assertion: model =>
        {
            model.Author.Should().NotBe("I DO SPAM!");
            model.Content.Should().NotContain("BUY MORE");
        });
}

<details> <summary><code>dotnet test</code> output when this assertion fails <em>(here: <code>Author</code> was expected to be <code>"I DO SPAM!"</code>)</em></summary>

  Failed Sample.Api.Tests.CommentsControllerTests.Get_WithCommentId_Returns_A_NonSpam_Comment [20 ms]
  Error Message:
   Expected response to satisfy one or more model assertions, but it wasn't: 

    - expected model.Author to be "I DO SPAM!" with a length of 10, but "Adrian" has a length of 6, differs near "Adr" (index 0).


The HTTP response was:

HTTP/1.1 200 OK
x-vendor: vendor
X-Correlation-ID: 6a779339-4740-4166-95ee-04a6563fed8e
Content-Type: application/json; charset=utf-8

{
  "author": "Adrian",
  "content": "Hey",
  "commentId": 1
}

The originating HTTP request was:

GET http://localhost/api/comments/1 HTTP 1.1
  Stack Trace:
     ...

</details>

  • Asserting the response has a header with the name X-Correlation-ID and the value matches a certain pattern
[Fact]
public async Task Get_Should_Contain_a_Header_With_Correlation_Id()
{
    // Arrange
    var client = _factory.CreateClient();

    // Act
    var response = await client.GetAsync("/api/values");

    // Assert
    response.Should().HaveHeader("X-Correlation-ID").And.Match("*-*", "we want to test the correlation id is a Guid like one");
}

<details> <summary><code>dotnet test</code> output when this assertion fails <em>(here: the pattern used was <code>-not-a-guid</code>)</em></summary>

  Failed Sample.Api.Tests.CommentsControllerTests.Get_Should_Contain_a_Header_With_Correlation_Id [13 ms]
  Error Message:
   Expected response to contain the HTTP header "X-Correlation-ID" having a value matching "*-not-a-guid*", but there was no match because we want to test the correlation id is a Guid like one. 

The HTTP response was:

HTTP/1.1 200 OK
X-Correlation-ID: 35f25ad0-dfac-4e78-9f0a-a13072dfce93
Content-Type: application/json; charset=utf-8

[
  "value1",
  "value2"
]

The originating HTTP request was:

GET http://localhost/api/values HTTP 1.1
  Stack Trace:
     ...

</details>

  • Asserting the response has a header with the name x-vendor and the value is not empty
[Fact]
public async Task Get_Should_Contain_a_NonEmpty_Header_With_Vendor()
{
    // Arrange
    var client = _factory.CreateClient();

    // Act
    var response = await client.GetAsync("/api/comments/1");

    // Assert
    response.Should().HaveHeader("x-vendor").And.NotBeEmpty();
}

<details> <summary><code>dotnet test</code> output when this assertion fails <em>(here: the endpoint called did not return the header at all — <code>GET /api/comments</code> instead of <code>GET /api/comments/1</code>)</em></summary>

  Failed Sample.Api.Tests.CommentsControllerTests.Get_Should_Contain_a_NonEmpty_Header_With_Vendor [3 ms]
  Error Message:
   Expected response to contain the HTTP header "x-vendor", but no such header was found in the actual response.

The HTTP response was:

HTTP/1.1 200 OK
X-Correlation-ID: e78196c0-438e-402a-b90a-baddfb3c6e01
Content-Type: application/json; charset=utf-8

[
  {
    "author": "Adrian",
    "content": "Hey",
    "commentId": 1
  },
  {
    "author": "Johnny",
    "content": "Hey!",
    "commentId": 2
  }
]

The originating HTTP request was:

GET http://localhost/api/comments HTTP 1.1
  Stack Trace:
     ...

</details>

Many more examples can be found in the Samples projects and in the Specs files from the FluentAssertions.Web.Tests project

Optional Global Configuration

Deserialization

System.Text.Json

By default System.Text.Json is used to deserialize the response content. The related System.Text.Json.JsonSerializerOptions used to configure the serializer is accessible via the SystemTextJsonSerializerConfig.Options static field from FluentAssertions.Web. So if you want to make the serializer case sensitive, then the related setting is changed like this:

SystemTextJsonSerializerConfig.Options.PropertyNameCaseInsensitive = false; 

The change must be done before the test is run and this depends on the testing framework. Check the NewtonsoftSerializerTests from this repo to see how it can be done with xUnit.

Newtonsoft.Json

The serializer itself is replaceable, so you can implement your own, by implementing the ISerializer interface. The serializer is shipped via the FluentAssertions.Web.Serializers.NewtonsoftJson and AwesomeAssertions.Web.Serializers.NewtonsoftJson package.

NuGet

To set the default serializer to Newtonsoft.Json one, use the following configuration:

FluentAssertionsWebConfig.Serializer = new NewtonsoftJsonSerializer();

or

AwesomeAssertionsWebConfig.Serializer = new NewtonsoftJsonSerializer();

The related Newtonsoft.Json.JsonSerializerSettings used to configure the Newtonsoft.Json serializer is accesible via the NewtonsoftJsonSerializerConfig.Options static field. So if you want to add a custom converter, then the related setting is changed like this:

NewtonsoftJsonSerializerConfig.Options.Converters.Add(new YesNoBooleanJsonConverter());

Response Formatting

The assertion failure messages include a readable rendering of the HTTP response (see When a test fails, you see the whole conversation for an example). By default, only the first 10 * 128 * 1024 bytes of the response content are printed, the rest being replaced by a warning message. To change this limit globally, set the ResponseFormatterOptions:

FluentAssertionsWebConfig.ResponseFormatterOptions = new HttpResponseFormatterOptions
{
    MaximumReadableBytes = 4 * 1024
};

or

AwesomeAssertionsWebConfig.ResponseFormatterOptions = new HttpResponseFormatterOptions
{
    MaximumReadableBytes = 4 * 1024
};

The change must be done before the test is run, like for the serializer configuration above. The global options only apply to the messages produced by FluentAssertions.Web and AwesomeAssertions.Web. The HttpMessageFormatter library itself keeps no global state and accepts the same HttpResponseFormatterOptions parameter object per call:

var formatted = response.Format(new HttpResponseFormatterOptions
{
    MaximumReadableBytes = 4 * 1024
});

Full API

The tables below list every assertion. Each group is followed by a short example and, where it helps, by the dotnet test output you get when the assertion does not hold, so it is clear what is actually being reported.

HttpResponseMessageAssertions Contains a number of methods to assert that an HttpResponseMessage is in the expected state related to the HTTP content.
Should().BeEmpty() Asserts that HTTP response content is empty.
Should().BeAs<TModel>() Asserts that HTTP response content can be an equivalent representation of the expected model.
Should().HaveHeader() Asserts that an HTTP response has a named header.
Should().NotHaveHeader() Asserts that an HTTP response does not have a named header.
Should().HaveHttpStatusCode() Asserts that an HTTP response has an HTTP status with the specified code.
Should().NotHaveHttpStatusCode() Asserts that an HTTP response does not have an HTTP status with the specified code.
Should().MatchInContent() Asserts that HTTP response has content that matches a wildcard pattern.
Should().Satisfy<TModel>() Asserts that the HTTP response content, once deserialized to TModel, satisfies an assertion.
Should().Satisfy() Asserts that the HttpResponseMessage itself satisfies an assertion.
response.Should().BeAs(new { Author = "John", Content = "Hey, you..." });
response.Should().HaveHttpStatusCode(HttpStatusCode.Accepted);
response.Should().MatchInContent("*\"commentId\": 1*");
response.Should().Satisfy<IEnumerable<Comment>>(comments => comments.Should().HaveCount(2));
response.Should().Satisfy(response => response.Headers.Contains("X-Correlation-ID"));

<details> <summary><code>dotnet test</code> output for <code>HaveHttpStatusCode(HttpStatusCode.Accepted)</code> when it fails</summary>

  Error Message:
   Expected response to be HttpStatusCode.Accepted {value: 202}, but found HttpStatusCode.Created {value: 201}.

The HTTP response was:

HTTP/1.1 201 Created
Location: http://localhost/api/Comments/1
X-Correlation-ID: 6ec3cb4b-36cf-4003-b95b-c2d577a9f739
Content-Type: application/json; charset=utf-8

{
  "author": "John",
  "content": "Hey, you...",
  "commentId": 1
}

The originating HTTP request was:

POST http://localhost/api/comments HTTP 1.1
Content-Type: application/json; charset=utf-8
Content-Length: 50
{
  "author": "John",
  "content": "Hey, you..."
}

</details>

Should().HaveHeader().And. Contains a number of methods to assert that an HttpResponseMessage is in the expected state related to HTTP headers.
BeEmpty() Asserts that an existing HTTP header in an HTTP response has no values.
NotBeEmpty() Asserts that an existing HTTP header in an HTTP response has any values.
BeValue() Asserts that an existing HTTP header in an HTTP response has an expected value.
BeValues() Asserts that an existing HTTP header in an HTTP response has an expected list of header values.
Match() Asserts that an existing HTTP header in an HTTP response contains at least a value that matches a wildcard pattern.
response.Should().HaveHeader("X-Correlation-ID").And.NotBeEmpty();
response.Should().HaveHeader("X-Correlation-ID").And.BeValue("5f1615eb-549e-4afa-a015-8c95fd8715c9");
response.Should().HaveHeader("Set-Cookie").And.BeValues(new[] { "a=1", "b=2" });
response.Should().HaveHeader("X-Correlation-ID").And.Match("*-*", "it should look like a Guid");

<details> <summary><code>dotnet test</code> output for <code>And.BeValue("other-vendor")</code> when it fails</summary>

  Error Message:
   Expected response to contain the "x-vendor" HTTP header and the expected header value to be equivalent to "other-vendor" with a length of 12, but "vendor" has a length of 6, differs near "ven" (index 0).

The HTTP response was:

HTTP/1.1 200 OK
x-vendor: vendor
X-Correlation-ID: e3903b0e-36b3-495d-a8f3-dfda4cee1566
Content-Type: application/json; charset=utf-8

{
  "author": "Adrian",
  "content": "Hey",
  "commentId": 1
}

The originating HTTP request was:

GET http://localhost/api/comments/1 HTTP 1.1

</details>

Should().Be400BadRequest().And. Contains a number of methods to assert that an HttpResponseMessage is in the expected state related to HTTP Bad Request response
HaveError() Asserts that a Bad Request HTTP response content contains an error message identifiable by an expected field name and a wildcard error text.
OnlyHaveError() Asserts that a Bad Request HTTP response content contains only a single error message identifiable by an expected field name and a wildcard error text.
NotHaveError() Asserts that a Bad Request HTTP response content does not contain an error message identifiable by an expected field name and a wildcard error text.
HaveErrorMessage() Asserts that a Bad Request HTTP response content contains an error message identifiable by an wildcard error text.
response.Should().Be400BadRequest()
    .And.HaveError("Author", "*required*")
    .And.NotHaveError("Content")
    .And.HaveErrorMessage("*one or more validation errors*");

<details> <summary><code>dotnet test</code> output for <code>HaveError()</code> when it fails</summary>

  Error Message:
   Expected response to contain an error message related to the "Content" field, but was not found.

The HTTP response was:

HTTP/1.1 400 BadRequest
X-Correlation-ID: c61ba4fa-ea03-484b-8345-ad6243e1e9ab
Content-Type: application/problem+json; charset=utf-8

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Author": [
      "The Author field is required."
    ]
  },
  "traceId": "00-aae99e6ddbee1a15d42f336faae193e3-1972a9b2677febe5-00"
}

The originating HTTP request was:

POST http://localhost/api/comments HTTP 1.1
Content-Type: application/json; charset=utf-8
Content-Length: 30
{
  "content": "Hey, you..."
}

</details>

HaveLocation Header related assertions.
Should().Be201Created().And.HaveLocation().And.BeValue() Asserts that an HTTP response with 201 status code has a location header.
Should().Be300Ambiguous().And.HaveLocation().And.BeValue() Asserts that an HTTP response with 300 status code has a location header.
Should().Be300MultipleChoices().And.HaveLocation().And.BeValue() Asserts that an HTTP response with 300 status code has a location header.
Should().Be301Moved().And.HaveLocation().And.BeValue() Asserts that an HTTP response with 301 status code has a location header.
Should().Be301MovedPermanently().And.HaveLocation().And.BeValue() Asserts that an HTTP response with 301 status code has a location header.
Should().Be302Found().And.HaveLocation().And.BeValue() Asserts that an HTTP response with 302 status code has a location header.
Should().Be302Redirect().And.HaveLocation().And.BeValue() Asserts that an HTTP response with 302 status code has a location header.
Should().Be303RedirectMethod().And.HaveLocation().And.BeValue() Asserts that an HTTP response with 303 status code has a location header.
Should().Be303SeeOther().And.HaveLocation().And.BeValue() Asserts that an HTTP response with 303 status code has a location header.
Should().Be304NotModified().And.HaveLocation().And.BeValue() Asserts that an HTTP response with 304 status code has a location header.
Should().Be305UseProxy().And.HaveLocation().And.BeValue() Asserts that an HTTP response with 305 status code has a location header.
Should().Be307RedirectKeepVerb().And.HaveLocation().And.BeValue() Asserts that an HTTP response with 307 status code has a location header.
Should().Be307TemporaryRedirect().And.HaveLocation().And.BeValue() Asserts that an HTTP response with 307 status code has a location header.
Should().Be308PermanentRedirect().And.HaveLocation().And.BeValue() Asserts that an HTTP response with 308 status code has a location header.
Should().Be3XXRedirection().And.HaveLocation().And.BeValue() Asserts that an HTTP redirection response has a location header.
response.Should().Be201Created()
    .And.HaveLocation()
    .And.Match("*/api/Comments/1");

<details> <summary><code>dotnet test</code> output for <code>Be201Created()</code> when the response is a 400 instead</summary>

  Error Message:
   Expected response to be HttpStatusCode.Created {value: 201}, but found HttpStatusCode.BadRequest {value: 400}.

The HTTP response was:

HTTP/1.1 400 BadRequest
X-Correlation-ID: 55b73f11-0235-4110-85d3-1ad3477fdd3e
Content-Type: application/problem+json; charset=utf-8

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Author": [
      "The Author field is required."
    ],
    "Content": [
      "The Content field is required."
    ]
  },
  "traceId": "00-88afb99689f9c20df02c7b18e409e45f-c1994f2de745b13d-00"
}

The originating HTTP request was:

POST http://localhost/api/comments HTTP 1.1
Content-Type: application/json; charset=utf-8
Content-Length: 19
{
  "content": ""
}

</details>

Fine grained status assertions.
Should().Be1XXInformational() Asserts that an HTTP response has an HTTP status code representing an informational response.
Should().Be2XXSuccessful() Asserts that an HTTP response has a successful HTTP status code.
Should().Be3XXRedirection() Asserts that an HTTP response has an HTTP status code representing a redirection response.
Should().Be4XXClientError() Asserts that an HTTP response has an HTTP status code representing a client error.
Should().Be5XXServerError() Asserts that an HTTP response has an HTTP status code representing a server error.
Should().Be100Continue() Asserts that an HTTP response has the HTTP status 100 Continue
Should().Be101SwitchingProtocols() Asserts that an HTTP response has the HTTP status 101 Switching Protocols
Should().Be200Ok() Asserts that an HTTP response has the HTTP status 200 Ok
Should().Be201Created() Asserts that an HTTP response has the HTTP status 201 Created
Should().Be202Accepted() Asserts that an HTTP response has the HTTP status 202 Accepted
Should().Be203NonAuthoritativeInformation() Asserts that an HTTP response has the HTTP status 203 Non Authoritative Information
Should().Be204NoContent() Asserts that an HTTP response has the HTTP status 204 No Content
Should().Be205ResetContent() Asserts that an HTTP response has the HTTP status 205 Reset Content
Should().Be206PartialContent() Asserts that an HTTP response has the HTTP status 206 Partial Content
Should().Be300Ambiguous() Asserts that an HTTP response has the HTTP status 300 Ambiguous
Should().Be300MultipleChoices() Asserts that an HTTP response has the HTTP status 300 Multiple Choices
Should().Be301Moved() Asserts that an HTTP response has the HTTP status 301 Moved
Should().Be301MovedPermanently() Asserts that an HTTP response has the HTTP status 301 Moved Permanently
Should().Be302Found() Asserts that an HTTP response has the HTTP status 302 Found
Should().Be302Redirect() Asserts that an HTTP response has the HTTP status 302 Redirect
Should().Be303RedirectMethod() Asserts that an HTTP response has the HTTP status 303 Redirect Method
Should().Be303SeeOther() Asserts that an HTTP response has the HTTP status 303 See Other
Should().Be304NotModified() Asserts that an HTTP response has the HTTP status 304 Not Modified
Should().Be305UseProxy() Asserts that an HTTP response has the HTTP status 305 Use Proxy
Should().Be306Unused() Asserts that an HTTP response has the HTTP status 306 Unused
Should().Be307RedirectKeepVerb() Asserts that an HTTP response has the HTTP status 307 Redirect Keep Verb
Should().Be307TemporaryRedirect() Asserts that an HTTP response has the HTTP status 307 Temporary Redirect
Should().Be308PermanentRedirect() Asserts that an HTTP response has the HTTP status 308 Permanent Redirect
Should().Be400BadRequest() Asserts that an HTTP response has the HTTP status 400 BadRequest
Should().Be401Unauthorized() Asserts that an HTTP response has the HTTP status 401 Unauthorized
Should().Be402PaymentRequired() Asserts that an HTTP response has the HTTP status 402 Payment Required
Should().Be403Forbidden() Asserts that an HTTP response has the HTTP status 403 Forbidden
Should().Be404NotFound() Asserts that an HTTP response has the HTTP status 404 Not Found
Should().Be405MethodNotAllowed() Asserts that an HTTP response has the HTTP status 405 Method Not Allowed
Should().Be406NotAcceptable() Asserts that an HTTP response has the HTTP status 406 Not Acceptable
Should().Be407ProxyAuthenticationRequired() Asserts that an HTTP response has the HTTP status 407 Proxy Authentication Required
Should().Be408RequestTimeout() Asserts that an HTTP response has the HTTP status 408 Request Timeout
Should().Be409Conflict() Asserts that an HTTP response has the HTTP status 409 Conflict
Should().Be410Gone() Asserts that an HTTP response has the HTTP status 410 Gone
Should().Be411LengthRequired() Asserts that an HTTP response has the HTTP status 411 Length Required
Should().Be412PreconditionFailed() Asserts that an HTTP response has the HTTP status 412 Precondition Failed
Should().Be413RequestEntityTooLarge() Asserts that an HTTP response has the HTTP status 413 Request Entity Too Large
Should().Be414RequestUriTooLong() Asserts that an HTTP response has the HTTP status 414 Request Uri Too Long
Should().Be415UnsupportedMediaType() Asserts that an HTTP response has the HTTP status 415 Unsupported Media Type
Should().Be416RequestedRangeNotSatisfiable() Asserts that an HTTP response has the HTTP status 416 Requested Range Not Satisfiable
Should().Be417ExpectationFailed() Asserts that an HTTP response has the HTTP status 417 Expectation Failed
Should().Be418ImATeapot() Asserts that an HTTP response has the HTTP status 418 I'm A Teapot
Should().Be422UnprocessableEntity() Asserts that an HTTP response has the HTTP status 422 Unprocessable Entity
Should().Be426UpgradeRequired() Asserts that an HTTP response has the HTTP status 426 UpgradeRequired
Should().Be429TooManyRequests() Asserts that an HTTP response has the HTTP status 429 Too Many Requests
Should().Be500InternalServerError() Asserts that an HTTP response has the HTTP status 500 Internal Server Error
Should().Be501NotImplemented() Asserts that an HTTP response has the HTTP status 501 Not Implemented
Should().Be502BadGateway() Asserts that an HTTP response has the HTTP status 502 Bad Gateway
Should().Be503ServiceUnavailable() Asserts that an HTTP response has the HTTP status 503 Service Unavailable
Should().Be504GatewayTimeout() Asserts that an HTTP response has the HTTP status 504 Gateway Timeout
Should().Be505HttpVersionNotSupported() Asserts that an HTTP response has the HTTP status 505 Http Version Not Supported
response.Should().Be2XXSuccessful();
response.Should().Be404NotFound();
response.Should().Be5XXServerError();

A failure here looks exactly like the status code outputs shown above: the expected/actual status codes, then the full response and the originating request.

The HttpResponsesMessage assertions from FluentAssertions vs. FluentAssertions.Web

In the 6.4.0 release FluentAssertions introduced a set of related assertions: BeSuccessful, BeRedirection, HaveClientError, HaveServerError, HaveError, HaveStatusCode, NotHaveStatusCode.

This library can still be used with FluentAssertions and it did not become obsoleted, not only because of the rich set of assertions, but also for the comprehensive output messages that are displayed when the test fails, feature that is not present in the main library, but in FluentAssertions.Web one.

FluentAssertions.Web vs FluentAssertions.Mvc vs FluentAssertions.Http

FluentAssertions.Web does not extend the assertions for the ASP.NET Core Controllers, if you are looking for that, then consider FluentAssertions.Mvc.

When FluentAssertions.Web was created, FluentAssertions.Http also existed at the time, solving the same problem when considering the asserting language. Besides the extra assertions added by FluentAssertions.Web, an important effort is put by this library on what happens when a test fails.

FluentAssertions 8.0.0 and beyond

Starting 8.0.0, FA is not an FOSS anymore. FluentAssertions.Web will maintain both FOSS (< 8.0.0) and the Commercial versions of FA (>= 8.0.0), so they will be deployed as separate Nuget packages: - FluentAssertions.Web will continue to dependend on the FOSS versions - FluentAssertions.Web.v8 will dependend on the Commercial versions

HttpResponse Formatter

The internal HTTP Request/Response formatter used by FluentAssertions.Web is now published as a standalone package, so it can be reused in other projects.

Basic usage:

  • start from an HTTPResponseMessage instance
  • reference the HttpMessageFormatter package from Nuget
  • import the extension method using HttpMessageFormatter;
  • call the Format method on the HttpResponseMessage instance
using HttpMessageFormatter;

public class HttpResponseFormatterExtensionsTests
{
   [Fact]
   public void GivenUnspecifiedResponse_ShouldFormatBasicResponse()
   {
       // Arrange
       using var subject = new HttpResponseMessage();

       // Act
       var formatted = subject.Format();

       // Assert
       Assert.Contains("The HTTP response was:", formatted);
       Assert.Contains("HTTP/1.1 200 OK", formatted);
   }
}

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on FluentAssertions.Web:

Package Downloads
Ark.Tools.SpecFlow

SpecfFlow utilities

GitHub repositories (4)

Showing the top 4 popular GitHub repositories that depend on FluentAssertions.Web:

Repository Stars
mehdihadeli/food-delivery-microservices
🍔 A practical and cloud-native food delivery microservices, built with .Net Aspire, .Net 10, Wolverine, Domain-Driven Design, CQRS, Vertical Slice Architecture, Event-Driven Architecture, and the latest technologies.
CodeMazeBlog/CodeMazeGuides
Source code for 800+ C# and .NET articles from code-maze.com. Every article is a runnable solution, most with tests.
mehdihadeli/food-delivery-modular-monolith
🌭 A practical and imaginary food and grocery delivery modular monolith, built with .Net 8, Domain-Driven Design, CQRS, Vertical Slice Architecture, Event-Driven Architecture, and the latest technologies.
mehdihadeli/vertical-slice-api-template
🍰 An asp.net core template based on .Net 10, Vertical Slice Architecture, CQRS, Minimal APIs, OpenTelemetry, API Versioning and OpenAPI.
Version Downloads Last Updated
3.0.0 2,115 10/9/2026
2.0.5 947 10/7/2026
2.0.4 872 10/2/2026
2.0.3 15,785 7/30/2026
2.0.2 456 7/28/2026
2.0.1 298 7/28/2026
2.0.0 563 7/23/2026
1.9.9 7,423 7/23/2026
1.9.5 363,158 5/3/2025
1.8.0 148,478 1/20/2025
1.7.0 127,133 11/29/2024
1.5.0 162,395 9/2/2024
1.4.0 158,131 5/24/2024
0.0.0-ci-286 85 10/2/2026
0.0.0-ci-285 97 10/2/2026
0.0.0-ci-252 128 7/23/2026
Loading failed