Skip to content

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.ListAllCalls3
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: IHttpClientFactory registers the real client first, test client is skipped
  • In tests: If no real HttpClient is 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.