FluentAssertions.Web
2.0.5
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
<PackageReference Include="FluentAssertions.Web" Version="2.0.5" />
<PackageVersion Include="FluentAssertions.Web" Version="2.0.5" />
<PackageReference Include="FluentAssertions.Web" />
paket add FluentAssertions.Web --version 2.0.5
#r "nuget: FluentAssertions.Web, 2.0.5"
#:package FluentAssertions.Web@2.0.5
#addin nuget:?package=FluentAssertions.Web&version=2.0.5
#tool nuget:?package=FluentAssertions.Web&version=2.0.5
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:
A couple of details about that output:
- when there is no originating request (for example because the
HttpResponseMessagewas created by hand), the last section readsThe 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:
Focus on the Assert part and not on the HttpClient related APIs, neither on the response deserialization
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().Resultand 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
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-IDand 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-vendorand 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.
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 | Versions 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. |
-
.NETStandard 2.0
- FluentAssertions (>= 6.5.1 && < 8.0.0)
- HttpMessageFormatter (>= 2.0.5)
- Microsoft.AspNetCore.WebUtilities (>= 2.3.0)
- System.Net.Http.Json (>= 8.0.1)
- System.Text.Encodings.Web (>= 8.0.0)
- System.Text.Json (>= 8.0.5)
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 |