Testing HTTP Clients¶
Every [HttpClient] generates a Test{TypeName} class — a test double that implements the client interface with call recording and configurable responses. No mocking libraries needed.
Generated Test Client¶
For this client:
[HttpClient]
public partial class UsersClient
{
[Get("/users/{id}")]
public partial Task<User> GetUser(int id);
[Post("/users")]
public partial Task<User> CreateUser([Body] CreateUserRequest request);
}
The generator produces:
public class TestUsersClient : IUsersClient
{
// Call recording — what was called and with what arguments
public IReadOnlyList<int> GetUserCalls => _getUserCalls;
public IReadOnlyList<CreateUserRequest> CreateUserCalls => _createUserCalls;
// Configurable responses — set before calling
public User GetUserResult { get; set; } = default!;
public User CreateUserResult { get; set; } = default!;
public Task<User> GetUser(int id, CancellationToken token = default)
{
_getUserCalls.Add(id);
return Task.FromResult(GetUserResult);
}
public Task<User> CreateUser(CreateUserRequest body, CancellationToken token = default)
{
_createUserCalls.Add(body);
return Task.FromResult(CreateUserResult);
}
}
Usage in Tests¶
Direct Usage¶
var client = new TestUsersClient
{
GetUserResult = new User(42, "Alice")
};
var user = await client.GetUser(42);
await Assert.That(user.Name).IsEqualTo("Alice");
await Assert.That(client.GetUserCalls).HasSingleItem().And.Contains(42);
With TestRuntime¶
When using effects, configure the test client on the runtime with .WithUsersClient():
var testClient = new TestUsersClient
{
GetUserResult = new User(42, "Alice"),
CreateUserResult = new User(43, "Bob")
};
var runtime = TestAppRuntime.Create()
.WithUsersClient(testClient);
var program =
from user in UsersClientEffects.UsersClient.GetUser<TestAppRuntime>(42)
select user;
var result = await program.RunAsync(runtime);
await Assert.That(result).IsSuccMatching(u => u.Name == "Alice");
You can also use the generated stub builder for inline configuration:
var runtime = TestAppRuntime.Create()
.WithStubUsersClient(stub => stub
.OnGetUser(async id => new User(id, "Alice")));
Call Recording Shapes¶
The shape of the Calls property depends on the method's parameter count:
| Parameters | Recording Type | Example |
|---|---|---|
| 0 | int CallCount |
client.ListAllCalls → 3 |
| 1 | IReadOnlyList<T> |
client.GetUserCalls → [42, 7] |
| 2+ | IReadOnlyList<(T1, T2, ...)> |
client.SearchCalls → [("alice", 10)] |
DI Registration¶
The test client is registered as a TryAddSingleton fallback in the generated Add{TypeName}() method. This means:
- In production:
IHttpClientFactoryregisters the real client first, test client is skipped - In tests: If no real
HttpClientis configured, the test client is resolved automatically
// Generated registration
services.AddHttpClient<IUsersClient, UsersClient>(); // real client (wins)
services.TryAddSingleton<IUsersClient, TestUsersClient>(); // fallback
Testing only
Test clients are generated alongside production code but are only active when no real HTTP client is registered. They are not auto-enabled in development environments.