Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 47 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,49 @@ Unlike other HTTP mocking libraries, Mockly offers:

Mockly is created and maintained by [Dennis Doomen](https://github.com/dennisdoomen), also the creator of [FluentAssertions](https://fluentassertions.com/), [PackageGuard](https://github.com/dennisdoomen/packageguard), [Reflectify](https://github.com/dennisdoomen/reflectify), [Pathy](https://github.com/dennisdoomen/pathy) and the [.NET Library Starter Kit](https://github.com/dennisdoomen/dotnet-library-starter-kit). It's designed to work seamlessly with modern .NET testing practices and integrates naturally with FluentAssertions for expressive test assertions.

## Power in Simplicity

```csharp
using var mock = new HttpMock();

// 1. Match with full URL shortcuts and wildcards
// 2. Filter by specific query parameters
// 3. Use custom JSON options for serialization
var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };

mock.ForGet("https://api.github.com/repos/*/issues?state=open")
.WithQueryParam("page", "1")
.Using(options)
.RespondsWithJsonContent(new[] {
new { Id = 1, Title = "Found a bug" },
new { Id = 2, Title = "Feature request" }
});

// 4. Match Bearer tokens and other headers
// 5. Match request body using JSON equivalence
// 6. Capture requests for later verification
var creations = new RequestCollection();

mock.ForPost()
.WithPath("/api/users")
.WithBearerToken("secret-token")
.WithBody(new { Name = "John", Role = "Admin" })
.CollectingRequestsIn(creations)
.RespondsWithStatus(HttpStatusCode.Created);

// 7. Built-in support for Problem Details (RFC 7807)
mock.ForGet("/api/users/999")
.RespondsWithProblemDetails(HttpStatusCode.NotFound, "User not found");

// Get the pre-configured HttpClient and start testing!
var client = mock.GetClient();

// 8. Assert your expectations with FluentAssertions
mock.Should().HaveAllRequestsCalled();
creations.Should().ContainRequestFor("/api/users")
.Which.HasHeader("X-Trace-Id");
```

## Key Features

### 🎯 Fluent Request Matching
Expand Down Expand Up @@ -115,7 +158,7 @@ Registered mocks:

```csharp
var patches = new RequestCollection();
mock.ForPatch().WithPath("/api/update").CollectingRequestIn(patches);
mock.ForPatch().WithPath("/api/update").CollectingRequestsIn(patches);

// After test execution
patches.Count.Should().Be(3);
Expand All @@ -131,12 +174,12 @@ mock.Requests.Should().NotContainUnexpectedCalls();

// Assert JSON-equivalence using a JSON string (ignores formatting/ordering)
mock.Requests.Should().ContainRequest()
.WithBodyMatchingJson("{ \"id\": 1, \"name\": \"x\" }");
.WithBodyMatchingJson("{ \"id\": 1, \"name\": \"John\" }");

// Assert the body deserializes and is equivalent to an object graph
var expected = new { id = 1, name = "x" };
var expected = new { id = 1, name = "John" };

mock.Requests.Should().ContainRequestForUrl("http://localhost:7021/api/*")
mock.Requests.Should().ContainRequestFor("https://api.example.com/*")
.WithBodyEquivalentTo(expected);
```

Expand Down
82 changes: 19 additions & 63 deletions website/docs/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,23 @@ mock.ForPatch()

The object is serialized using `JsonSerializer` with default options and compared to the request body using JSON equivalence, ignoring differences in whitespace and layout.

#### Custom JSON Options

You can supply custom `JsonSerializerOptions` for the body matching:

```csharp
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

mock.ForPost()
.WithPath("/api/data")
.Using(options)
.WithBody(new { UserId = 42, UserName = "Alice" })
.RespondsWithStatus(HttpStatusCode.NoContent);
```

### Regular Expression

```csharp
Expand Down Expand Up @@ -166,7 +183,7 @@ var capturedRequests = new RequestCollection();

mock.ForPatch()
.WithPath("/api/update")
.CollectingRequestIn(capturedRequests)
.CollectingRequestsIn(capturedRequests)
.RespondsWithStatus(HttpStatusCode.NoContent);

// After making requests
Expand All @@ -176,65 +193,4 @@ capturedRequests.First().WasExpected.Should().BeTrue();

## Assertions

### Verify All Mocks Were Called

```csharp
mock.Should().HaveAllRequestsCalled();
```

### Verify No Unexpected Requests

```csharp
mock.Requests.Should().NotContainUnexpectedCalls();
```

### Verify Request Expectations

```csharp
var request = mock.Requests.First();
request.Should().BeExpected();
request.WasExpected.Should().BeTrue();
```

### Assert an Unexpected Request

```csharp
var first = mock.Requests.First();
first.Should().BeUnexpected();
```

### Collection Assertions

```csharp
mock.Requests.Should().NotBeEmpty();
mock.Requests.Should().HaveCount(3);
capturedRequests.Should().BeEmpty();
```

### Body Assertions on Captured Requests

Use these to assert on the JSON body of a previously captured request:

```csharp
// Assert JSON-equivalence using a JSON string (ignores formatting/ordering)
mock.Requests.Should().ContainRequest()
.WithBodyMatchingJson("{ \"id\": 1, \"name\": \"x\" }");

// Assert the body deserializes and is equivalent to an object graph
var expected = new { id = 1, name = "x" };
mock.Requests.Should().ContainRequest()
.WithBodyEquivalentTo(expected);

// Assert the body has specific properties (deserialized as a dictionary)
var expectedProps = new Dictionary<string, string>
{
["id"] = "1",
["name"] = "x"
};
mock.Requests.Should().ContainRequest()
.WithBodyHavingPropertiesOf(expectedProps);
```

:::info
These assertions operate on captured requests (`mock.Requests`). They are part of the FluentAssertions extensions shipped with Mockly. If you disabled `HttpMock.PrefetchBody`, `RequestInfo.Body` will be `null`; enable it when you need to assert on the body.
:::
Mockly provides extensive support for test assertions through **FluentAssertions**. For a full guide on available assertions for mocks, collections, and requests, see the [Assertions](./assertions.md) page.
142 changes: 142 additions & 0 deletions website/docs/assertions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
---
sidebar_position: 5
---

# Assertions

Mockly integrates naturally with [FluentAssertions](https://fluentassertions.com/) through the `FluentAssertions.Mockly` extension packages. These packages provide a set of intention-revealing extension methods to verify that your system under test interacted with the HTTP mocks as expected.

## Installation

Depending on which version of FluentAssertions you are using, install the corresponding Mockly assertion package:

### For FluentAssertions v8.x

```bash
dotnet add package FluentAssertions.Mockly.v8
```

### For FluentAssertions v7.x

```bash
dotnet add package FluentAssertions.Mockly.v7
```

## HttpMock Assertions

You can perform high-level assertions on the `HttpMock` instance itself to ensure all configured mocks were utilized.

```csharp
using var mock = new HttpMock();
mock.ForGet("/api/users").RespondsWithStatus(HttpStatusCode.OK);

// ... perform actions ...

// Verify that all configured mocks were called at least once
mock.Should().HaveAllRequestsCalled();

// Verify that mocks were called in a specific order
var mock1 = mock.ForGet("/api/first").RespondsWithStatus(HttpStatusCode.OK);
var mock2 = mock.ForGet("/api/second").RespondsWithStatus(HttpStatusCode.OK);

mock.Should().HaveCalledInOrder(mock1, mock2);
```

### Unexpected Requests

You can verify that no unexpected calls were made to the mock:

```csharp
mock.Requests.Should().NotContainUnexpectedCalls();
```

## Setup Assertions

If you keep a reference to a mock setup, you can assert on it directly.

```csharp
var userMock = mock.ForGet("/api/users/*").RespondsWithStatus(HttpStatusCode.OK);

// ... perform actions ...

userMock.Should().HaveBeenCalled();
userMock.Should().HaveBeenCalled(Because.Of("the user list should be refreshed"));
```

## Request Collection Assertions

When using a `RequestCollection` to capture requests, you can use specialized assertions to inspect the captured data.

```csharp
var captured = new RequestCollection();
mock.ForPost("/api/data").CollectingRequestsIn(captured).RespondsWithStatus(HttpStatusCode.OK);

// ... perform actions ...

// Check if any requests were captured at all
captured.Should().NotBeEmpty();
captured.Should().HaveCount(2);

// Assert on the presence of specific requests using URL patterns
captured.Should().ContainRequestFor("/api/data");
captured.Should().NotContainRequestFor("/api/other");
```

## Chaining Assertions

The `ContainRequestFor` assertion allows you to chain further checks on the matched request using the `Which` property.

```csharp
captured.Should().ContainRequestFor("/api/data")
.Which.HasHeader("X-Custom-Header", "ExpectedValue")
.And.WithBody("*part-of-body*")
.And.WithBearerToken();
```

### Available Chained Assertions

On the result of `ContainRequestFor(...).Which`, you can use:

- `HasHeader(name, [value])`: Verifies the presence and optionally the value of a header.
- `WithBody(pattern)`: Verifies the request body matches a string or wildcard pattern.
- `WithBearerToken()`: Verifies that a Bearer token is present in the `Authorization` header.
- `WithQuery(query)`: Verifies the request query string.

## Body Assertions on Captured Requests

Use these specialized assertions to verify the JSON body of captured requests:

```csharp
// Assert JSON-equivalence using a JSON string (ignores formatting/ordering)
mock.Requests.Should().ContainRequest()
.WithBodyMatchingJson("{ \"id\": 1, \"name\": \"John\" }");

// Assert the body deserializes and is equivalent to an object graph
var expected = new { id = 1, name = "John" };
mock.Requests.Should().ContainRequest()
.WithBodyEquivalentTo(expected);

// Assert the body has specific properties (deserialized as a dictionary)
var expectedProps = new Dictionary<string, string>
{
["id"] = "1",
["name"] = "John"
};
mock.Requests.Should().ContainRequest()
.WithBodyHavingPropertiesOf(expectedProps);
```

:::info
These assertions require the request body to be available in memory. If you disabled `HttpMock.PrefetchBody`, these assertions will fail as `RequestInfo.Body` will be `null`.
:::

## Individual Request Assertions

You can also assert on individual `CapturedRequest` objects.

```csharp
var request = captured.First();

request.Should().BeExpected();
request.Should().NotBeUnexpected();
```
2 changes: 1 addition & 1 deletion website/docs/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Registered mocks:

```csharp
var patches = new RequestCollection();
mock.ForPatch().WithPath("/api/update").CollectingRequestIn(patches);
mock.ForPatch().WithPath("/api/update").CollectingRequestsIn(patches);

// After test execution
patches.Count.Should().Be(3);
Expand Down
6 changes: 4 additions & 2 deletions website/docs/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ mock.ForGet()
HttpClient client = mock.GetClient();

// Act

// Note: BaseAddress defaults to https://localhost/
var response = await client.GetAsync("/api/users/123");
var content = await response.Content.ReadAsStringAsync();
Expand Down Expand Up @@ -91,7 +92,7 @@ using FluentAssertions;
public class UserServiceTests
{
[Fact]
public async Task Should_Handle_User_Operations()
public async Task Can_create_and_update_a_user()
{
// Arrange
var mock = new HttpMock();
Expand All @@ -108,7 +109,7 @@ public class UserServiceTests

mock.ForPatch()
.WithPath("/api/users/*")
.CollectingRequestIn(capturedPatches)
.CollectingRequestsIn(capturedPatches)
.RespondsWithStatus(HttpStatusCode.NoContent);

HttpClient client = mock.GetClient();
Expand Down Expand Up @@ -136,5 +137,6 @@ public class UserServiceTests
Now that you have Mockly set up, explore the following topics:

- [Usage](usage.md) - Learn about basic mocking patterns
- [Assertions](assertions.md) - Learn about intention-revealing test assertions
- [Advanced Features](advanced.md) - Dive into custom matchers, request capture, and more
- [Building](building.md) - Information about building the project from source
Loading
Loading