Usage
Learn about the core features and how to use Mockly effectively in your tests.
Basic Mocking
Create an HttpMock instance and configure it using the fluent API:
var mock = new HttpMock();
// Mock a GET request
mock.ForGet()
.WithPath("/api/products")
.RespondsWithJsonContent(new[]
{
new { Id = 1, Name = "Product 1" },
new { Id = 2, Name = "Product 2" }
});
// Get the HttpClient
HttpClient client = mock.GetClient(); // BaseAddress defaults to https://localhost/
Import From cURL
Bootstrap a mock from an existing curl command, such as the output of a browser's "Copy as cURL":
mock.ImportFromCurl("""
curl -X POST https://api.example.com/users \
-H 'Content-Type: application/json' \
--data-raw '{"name":"mockly"}'
""")
.RespondsWithStatus(HttpStatusCode.Created);
The method (-X), URL, headers (-H) and body (-d/--data/--data-raw) are translated into the equivalent
matching configuration. When no method is specified, POST is assumed if a body is present and GET otherwise.
Attach a response to the returned builder, just like any other mock, to complete the configuration.
Getting an HttpClient, IHttpClientFactory or HttpMessageHandler
Mockly provides three ways to wire the mock into your code under test:
GetClient()— returns a newHttpClientwithBaseAddressset tohttps://localhost/, ready to use directly in tests.GetClientFactory()— returns anIHttpClientFactorywhoseCreateClient()method producesHttpClientinstances backed by the mock. Use this when your code depends onIHttpClientFactory.GetMessageHandler()— returns the underlyingHttpMessageHandler. Use this when you need to build a customHttpClientor pass the handler to other infrastructure.
// Option 1: HttpClient (BaseAddress defaults to https://localhost/)
HttpClient client = mock.GetClient();
// Option 2: IHttpClientFactory
IHttpClientFactory factory = mock.GetClientFactory();
HttpClient clientFromFactory = factory.CreateClient("myClient");
// Option 3: HttpMessageHandler
HttpMessageHandler handler = mock.GetMessageHandler();
var customClient = new HttpClient(handler) { BaseAddress = new Uri("https://localhost/") };
HTTP Method Support
Mockly supports all common HTTP methods:
mock.ForGet() // GET requests
mock.ForPost() // POST requests
mock.ForPut() // PUT requests
mock.ForPatch() // PATCH requests
mock.ForDelete() // DELETE requests
mock.ForHead() // HEAD requests
mock.ForOptions() // OPTIONS requests
// Generic method support for any verb
mock.For(HttpMethod.Get)
mock.For(new HttpMethod("PROPFIND"))
Full-URL Shortcuts
Instead of configuring scheme/host/path/query separately, you can provide a single full URL pattern to each ForXxx method. Wildcards (*) are supported in the host, path, and query parts.
// GET: full https URL, wildcard path and query
mock.ForGet("https://api.example.com/users/*?q=*")
.RespondsWithStatus(HttpStatusCode.OK);
// POST: wildcard host and path
mock.ForPost("http://*.example.com/*")
.RespondsWithStatus(HttpStatusCode.Created);
// PUT: full https URL with wildcard path and query
mock.ForPut("https://api.example.com/items/*?filter=*")
.RespondsWithStatus(HttpStatusCode.OK);
// PATCH: wildcard host and path
mock.ForPatch("http://*.contoso.local/*")
.RespondsWithStatus(HttpStatusCode.OK);
// DELETE: specific host and path
mock.ForDelete("http://localhost/api/items/*")
.RespondsWithEmptyContent(HttpStatusCode.NoContent);
Path and Query Matching
Exact Matching
mock.ForGet().WithPath("/api/users/123");
Wildcard Matching
// Match any user ID
mock.ForGet().WithPath("/api/users/*");
// Match query parameters with wildcards
mock.ForGet()
.WithPath("/api/search")
.WithQuery("?q=*&limit=10");
// Match any query string
mock.ForGet().WithPath("/api/data").WithAnyQuery();
// Match requests without any query string
mock.ForGet().WithPath("/api/data").WithoutQuery();
Query Parameter Matching
Match individual query parameters regardless of their order in the URI:
mock.ForGet()
.WithPath("/api/search")
.WithQueryParam("q", "mockly*")
.WithQueryParam("page", "1")
.RespondsWithStatus(HttpStatusCode.OK);
You can also match by presence only:
mock.ForGet()
.WithPath("/api/search")
.WithQueryParam("api-key")
.RespondsWithStatus(HttpStatusCode.OK);
Form Field Matching
Match application/x-www-form-urlencoded request bodies by field name and value:
mock.ForPost()
.WithPath("/oauth/token")
.WithFormField("grant_type", "client_credentials")
.WithFormField("scope", "read*")
.RespondsWithStatus(HttpStatusCode.OK);
Header Matching
Use the first-class header matchers when you need to assert on common request metadata:
mock.ForGet()
.WithPath("/api/secure")
.WithHeader("X-Api-Key")
.RespondsWithStatus(HttpStatusCode.OK);
mock.ForGet()
.WithPath("/api/secure")
.WithHeader("X-Trace-Id", "abc-*")
.RespondsWithStatus(HttpStatusCode.OK);
mock.ForGet()
.WithPath("/api/auth")
.WithBearerToken("eyJ*")
.RespondsWithStatus(HttpStatusCode.OK);
mock.ForPost()
.WithPath("/api/json")
.WithContentType("application/json")
.RespondsWithStatus(HttpStatusCode.OK);
WithHeader(name, valuePattern) matches when any value of a multi-valued header satisfies the wildcard pattern. WithContentType matches the media type and ignores parameters such as charset.
Response Configuration
JSON Responses
mock.ForGet()
.WithPath("/api/user")
.RespondsWithJsonContent(new { Id = 1, Name = "John" });
Custom JSON Options
You can supply custom JsonSerializerOptions for serialization:
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
mock.ForGet()
.WithPath("/api/user")
.Using(options)
.RespondsWithJsonContent(new { UserId = 1, UserName = "John" });
Problem Details Responses
Mockly has built-in support for RFC 7807 Problem Details:
mock.ForGet()
.WithPath("/api/users/999")
.RespondsWithProblemDetails(
HttpStatusCode.NotFound,
title: "User not found",
detail: "No user exists with id 999",
type: "https://example.com/problems/not-found",
instance: "/api/users/999",
extensions: new Dictionary<string, object?>
{
["traceId"] = "00-abc-def-01"
});
Assertions
Mockly integrates with FluentAssertions for expressive and intention-revealing test assertions. See the Assertions page for details on how to verify your HTTP interactions.
String Content
mock.ForGet()
.WithPath("/api/text")
.RespondsWithContent("Hello, World!", "text/plain");
Status Codes
mock.ForPost()
.WithPath("/api/create")
.RespondsWithStatus(HttpStatusCode.Created);
Response Headers
Use WithHeader to add response headers such as Location, ETag or Cache-Control. Content headers
(Content-Type, Content-Length, etc.) are routed to the response content automatically; all other headers are
added to the response headers. Pass multiple values to add a multi-valued header.
mock.ForPost()
.WithPath("/api/users")
.RespondsWithStatus(HttpStatusCode.Created)
.WithHeader("Location", "/api/users/123")
.WithHeader("ETag", "\"v1\"");
// Multi-valued header
mock.ForGet()
.WithPath("/api/data")
.RespondsWithStatus(HttpStatusCode.OK)
.WithHeader("X-Custom", "first", "second");
Empty Responses
mock.ForDelete()
.WithPath("/api/resource/123")
.RespondsWithEmptyContent(HttpStatusCode.NoContent);
HTTP Content Responses
// Simple content (defaults to 200 OK)
var content = new ByteArrayContent(imageBytes);
mock.ForGet()
.WithPath("/api/image")
.RespondsWith(content);
// Complex content with status code
var inner = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(json, Encoding.UTF8, "application/json")
};
var multipart = new MultipartContent("mixed", $"batch_{Guid.NewGuid()}");
multipart.Add(new HttpMessageContent(inner));
mock.ForPost()
.WithPath("/api/batch")
.RespondsWith(HttpStatusCode.OK, multipart);
File, Stream and Byte Responses
For large or binary payloads (file downloads, images, PDFs) you can stream a file, a raw byte array, or an arbitrary Stream directly:
// Stream a file; the file is opened freshly per request so the mock can be called multiple times.
// The content type is inferred from the extension (defaults to application/octet-stream) unless supplied.
mock.ForGet()
.WithPath("/api/report")
.RespondsWithFile("report.pdf");
mock.ForGet()
.WithPath("/api/logo")
.RespondsWithFile("logo.dat", "image/png");
// Raw bytes are buffered, so the mock can safely be invoked multiple times.
mock.ForGet()
.WithPath("/api/bytes")
.RespondsWithBytes(imageBytes, "image/png");
// A Stream can only be consumed once. Prefer RespondsWithBytes or RespondsWithFile when the
// mock may be called more than once.
mock.ForGet()
.WithPath("/api/stream")
.RespondsWithStream(stream, "application/octet-stream");
Custom Responses
mock.ForGet()
.WithPath("/api/custom")
.RespondsWith(request =>
{
var response = new HttpResponseMessage(HttpStatusCode.OK);
response.Headers.Add("X-Custom-Header", "value");
response.Content = new StringContent("Custom content");
return response;
});
A custom responder can also be asynchronous, which is useful when producing the response requires awaiting something (e.g. reading a file or calling another service):
mock.ForGet()
.WithPath("/api/custom")
.RespondsWith(async request =>
{
string content = await File.ReadAllTextAsync("response.json");
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(content)
};
});
Add a CancellationToken parameter to observe the token flowing from the HTTP pipeline, for example to pass it on
to an awaited call:
mock.ForGet()
.WithPath("/api/custom")
.RespondsWith(async (request, cancellationToken) =>
{
string content = await File.ReadAllTextAsync("response.json", cancellationToken);
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(content)
};
});
Sequenced Responses
Configure a sequence of responses for the same matched request so consecutive calls get different results:
mock.ForGet()
.WithPath("/api/resource")
.RespondsWithStatus(HttpStatusCode.ServiceUnavailable)
.ThenRespondsWithStatus(HttpStatusCode.ServiceUnavailable)
.ThenRespondsWithStatus(HttpStatusCode.OK);
ThenRespondsWith* mirrors the RespondsWith* response methods, including JSON/content/OData variants and custom delegates (ThenRespondsWith(Func<RequestInfo, HttpResponseMessage>)).
Behavior notes:
- After the configured sequence is exhausted, the last response is reused.
- Sequence length is independent from invocation limits (
Once(),Twice(),Times(n)).
Using Test Data Builders
Mockly supports the IResponseBuilder<T> interface, allowing you to integrate test data builders seamlessly:
Implementing a Test Data Builder
public class UserBuilder : IResponseBuilder<User>
{
private int id = 1;
private string name = "Default User";
private string email = "[email protected]";
public UserBuilder WithId(int id)
{
this.id = id;
return this;
}
public UserBuilder WithName(string name)
{
this.name = name;
return this;
}
public UserBuilder WithEmail(string email)
{
this.email = email;
return this;
}
public User Build()
{
return new User { Id = id, Name = name, Email = email };
}
}
Using Builders with JSON Responses
var userBuilder = new UserBuilder()
.WithId(123)
.WithName("John Doe")
.WithEmail("[email protected]");
mock.ForGet()
.WithPath("/api/user")
.RespondsWithJsonContent(userBuilder);
// With custom status code
mock.ForPost()
.WithPath("/api/user")
.RespondsWithJsonContent(HttpStatusCode.Created, userBuilder);
Using Builders with OData Responses
// Single item
var userBuilder = new UserBuilder().WithId(1).WithName("Alice");
mock.ForGet()
.WithPath("/odata/user")
.RespondsWithODataResult(userBuilder);
// Collection of items
var builders = new[]
{
new UserBuilder().WithId(1).WithName("Alice"),
new UserBuilder().WithId(2).WithName("Bob"),
new UserBuilder().WithId(3).WithName("Charlie")
};
mock.ForGet()
.WithPath("/odata/users")
.RespondsWithODataResult(builders);
// With custom status code and OData context
mock.ForGet()
.WithPath("/odata/users")
.RespondsWithODataResult(
HttpStatusCode.OK,
builders,
"https://localhost/$metadata#Users");
Request Inspection
Access all captured requests globally:
var allRequests = mock.Requests;
allRequests.Count.Should().Be(5);
allRequests.Should().NotContainUnexpectedCalls();
var firstRequest = allRequests.First();
firstRequest.Method.Should().Be(HttpMethod.Get);
firstRequest.Path.Should().StartWith("/api/");
firstRequest.Timestamp.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
Handling Unexpected Requests
By default, unexpected requests throw an exception:
var mock = new HttpMock();
mock.FailOnUnexpectedCalls = true; // Default
// This will throw UnexpectedRequestException
await client.GetAsync("http://localhost/unmocked-path");
To allow unexpected requests:
mock.FailOnUnexpectedCalls = false;
// Returns 404 NotFound instead of throwing
var response = await client.GetAsync("http://localhost/unmocked-path");
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
Continuing Configuration
You can continue adding mocks to an existing HttpMock:
var mock = new HttpMock();
// Initial configuration
mock.ForGet().WithPath("/api/users").RespondsWithStatus(HttpStatusCode.OK);
var client = mock.GetClient();
// ... make some requests ...
// Add more mocks later
mock.ForPost().WithPath("/api/users").RespondsWithStatus(HttpStatusCode.Created);
// Same client works with new mocks
await client.PostAsync("http://localhost/api/users", content);
Builder Lifecycle
Continue building can reuse parts of the previous builder for convenience. You can opt out with Reset():
mock.ForGet()
.ForHttps().ForHost("somehost")
.WithPath("/api/test")
.WithQuery("?q=test")
.RespondsWithStatus(HttpStatusCode.OK);
// Reset prevents reusing the previous builder's scheme/host
mock.Reset();
mock.ForGet()
.WithPath("/api/test")
.WithQuery("?q=test")
.RespondsWithStatus(HttpStatusCode.NotModified);
Clearing Mocks
Reset all configured mocks:
mock.Clear();
OData Result Helpers
Produce OData-style envelopes directly from the builder:
var items = new[] { new { Id = 1, Name = "A" }, new { Id = 2, Name = "B" } };
mock.ForGet()
.WithPath("/odata/items")
.RespondsWithODataResult(items);
// Empty result
mock.ForGet()
.WithPath("/odata/empty")
.RespondsWithODataResult(Array.Empty<object>());
// Include @odata.context and custom status code
mock.ForGet()
.WithPath("/odata/ctx")
.RespondsWithODataResult(items, context: "http://localhost/$metadata#items", statusCode: HttpStatusCode.OK);