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/

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

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");

Response Configuration

JSON Responses

mock.ForGet()
.WithPath("/api/user")
.RespondsWithJsonContent(new { Id = 1, Name = "John" });

String Content

mock.ForGet()
.WithPath("/api/text")
.RespondsWithContent("Hello, World!", "text/plain");

Status Codes

mock.ForPost()
.WithPath("/api/create")
.RespondsWithStatus(HttpStatusCode.Created);

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);

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;
});

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);