Advanced Features
Explore advanced features and patterns for power users.
Custom Matchers
Use predicates for advanced matching logic:
mock.ForGet()
.WithPath("/api/data")
.With(request => request.Headers.Contains("X-API-Key"))
.RespondsWithStatus(HttpStatusCode.OK);
Inspect Request Body
mock.ForPost()
.WithPath("/api/test")
.With(req => req.Body!.Contains("something"))
.RespondsWithStatus(HttpStatusCode.NoContent);
Async Predicate Matching
mock.ForGet()
.WithPath("/api/async")
.With(async req =>
{
await Task.Delay(1);
return req.Uri!.Query == "?q=test";
})
.RespondsWithStatus(HttpStatusCode.OK);
If no mock matches, an UnexpectedRequestException is thrown when FailOnUnexpectedCalls is true (default).
Body Matching
Match request bodies using different strategies:
Wildcard Pattern
mock.ForPost()
.WithPath("/api/test")
.WithBody("*something*")
.RespondsWithStatus(HttpStatusCode.NoContent);
JSON Equivalence
Layout and whitespace independent:
mock.ForPost()
.WithPath("/api/json")
.WithBodyMatchingJson("{\"name\": \"John\", \"age\": 30}")
.RespondsWithStatus(HttpStatusCode.NoContent);
If the body cannot be parsed as JSON for WithBodyMatchingJson, a RequestMatchingException is thrown.
Regular Expression
mock.ForPost()
.WithPath("/api/test")
.WithBodyMatchingRegex(".*something.*")
.RespondsWithStatus(HttpStatusCode.NoContent);
Request Body Prefetching
By default, Mockly prefetches the request body for matchers. You can disable this to defer reading content inside your predicate:
var mock = new HttpMock { PrefetchBody = false };
RequestInfo? captured = null;
mock.ForPost()
.WithPath("/api/test")
.With(req =>
{
captured = req; // req.Body can be read lazily here by your predicate
return true;
})
.RespondsWithStatus(HttpStatusCode.OK);
What PrefetchBody Does
- Purpose: When
PrefetchBodyistrue(default), Mockly eagerly reads and caches the HTTP request body intoRequestInfo.Bodyso that matchers and later assertions can inspect it without re-reading the stream. - When to disable: Turn it off for scenarios with large or streaming content where reading the body up front is expensive or undesirable. In that case,
RequestInfo.Bodywill benullunless your own predicate reads it. - Impact on assertions: Body-based assertions require the body to be available. Keep
PrefetchBodyenabled if you plan to assert on the request body after the call.
Limiting Mock Invocations
Sometimes you want a mock to respond only a limited number of times. You can restrict a mock using the fluent methods Once(), Twice(), or Times(int count) on the request builder.
var mock = new HttpMock();
// Single-use response
mock.ForGet()
.WithPath("/api/item")
.RespondsWithStatus(HttpStatusCode.OK)
.Once();
// Exactly two times
mock.ForPost()
.WithPath("/api/items")
.RespondsWithJsonContent(new { ok = true })
.Twice();
// Exactly N times
mock.ForDelete()
.WithPath("/api/items/*")
.RespondsWithEmptyContent()
.Times(3);
Behavior Notes
- Exhausted mocks are skipped when matching. If no other non-exhausted mock matches and
FailOnUnexpectedCallsistrue(default), anUnexpectedRequestExceptionis thrown. - The mocks are evaluated in the order they were created.
- The default for mocks without limits is unlimited invocations
- The verification helpers consider limits:
HttpMock.AllMocksInvokedreturnstrueonly when each mock has been called at least once or has reached its configuredTimes(..)limit.HttpMock.GetUninvokedMocks()lists mocks that haven't reached their required count (or have 0 calls for unlimited mocks).
Request Collection
Capture requests for specific mocks:
var capturedRequests = new RequestCollection();
mock.ForPatch()
.WithPath("/api/update")
.CollectingRequestIn(capturedRequests)
.RespondsWithStatus(HttpStatusCode.NoContent);
// After making requests
capturedRequests.Count.Should().Be(2);
capturedRequests.First().WasExpected.Should().BeTrue();
Assertions
Verify All Mocks Were Called
mock.Should().HaveAllRequestsCalled();
Verify No Unexpected Requests
mock.Requests.Should().NotContainUnexpectedCalls();
Verify Request Expectations
var request = mock.Requests.First();
request.Should().BeExpected();
request.WasExpected.Should().BeTrue();
Assert an Unexpected Request
var first = mock.Requests.First();
first.Should().BeUnexpected();
Collection Assertions
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:
// 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);
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.