Skip to main content

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 new HttpClient with BaseAddress set to https://localhost/, ready to use directly in tests.
  • GetClientFactory() — returns an IHttpClientFactory whose CreateClient() method produces HttpClient instances backed by the mock. Use this when your code depends on IHttpClientFactory.
  • GetMessageHandler() — returns the underlying HttpMessageHandler. Use this when you need to build a custom HttpClient or 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);