| name | tunit-mocking |
| description | TUnit's source-generated, AOT-compatible mocking framework โ TUnit.Mocks. Use whenever the user is mocking dependencies in TUnit tests, references Mock.Of<T>(), `.Mock()` extension methods, `Any()`, `Is<T>(...)`, `WasCalled(Times.Once)`, or asks how to stub HttpClient/ILogger/ILogger<T> in TUnit. Also use when the user mentions the TUnit.Mocks, TUnit.Mocks.Http, or TUnit.Mocks.Logging packages, or asks how to verify mock calls without Moq/NSubstitute. Covers mock creation, setup (Returns/Throws/Callback/sequences/properties), argument matchers, call verification with the Times constraints, ordered verification, HTTP request matching and verification, and ILogger entry assertions. |
Mocking with TUnit.Mocks
Help users mock dependencies in TUnit tests using TUnit's first-party, source-generated mock framework.
When to Use
- User is mocking interfaces or classes in TUnit tests
- User references
Mock.Of<T>(), .Mock(), Any(), Is<T>(...), WasCalled, Times.*
- User wants to stub an
HttpClient / HttpMessageHandler in a TUnit test
- User wants to assert log entries via
ILogger / ILogger<T> in a TUnit test
When Not to Use
- User is using Moq or NSubstitute (those have their own conventions; TUnit.Mocks is a separate library)
- User is writing a non-TUnit test (TUnit.Mocks targets TUnit; for other frameworks use Moq/NSubstitute)
- User wants test authoring guidance generally โ use
writing-tunit-tests
Inputs
| Input | Required | Description |
|---|
| Interface or virtual class to mock | Yes | The dependency being mocked |
| Test scenario | No | What behavior to verify |
Workflow
Step 1: Install the right package
TUnit.Mocks ships as separate packages so you only pay for what you use:
dotnet add package TUnit.Mocks --prerelease
dotnet add package TUnit.Mocks.Http --prerelease
dotnet add package TUnit.Mocks.Logging --prerelease
Requirements:
- C# 14 or later โ older language versions trigger
TM004 at build time. Set <LangVersion>latest</LangVersion> (or 14) in the test project.
- TUnit.Mocks is source-generated and AOT-compatible โ it works with NativeAOT, trimming, and single-file publishing without reflection at runtime.
Step 2: Create a mock
Two equivalent forms โ pick one and stay consistent:
using TUnit.Mocks;
var greeter = IGreeter.Mock();
var greeter = Mock.Of<IGreeter>();
The mock is the interface โ there is no .Object property to unwrap (unlike Moq):
IGreeter g = greeter;
service.Inject(greeter);
Step 3: Configure return values, throws, and callbacks
Use the same call shape on the mock as on the real interface, then chain a behavior:
mock.GetUser(Any()).Returns(new User("Alice"));
mock.GetUserAsync(Any()).Returns(new User("Alice"));
mock.GetTimestamp(Any()).Returns(() => DateTime.UtcNow);
mock.Delete(Any()).Throws<InvalidOperationException>();
mock.Delete(Any()).Throws(new ArgumentException("bad id"));
mock.Process(Any()).Callback(() => callCount++);
mock.Process(Any()).Callback((object?[] args) =>
Console.WriteLine($"Called with: {args[0]}"));
Sequences โ different behavior per call
When the same method should produce different results on successive calls, use .Then() to chain or .ReturnsSequentially(...) for value-only sequences:
mock.GetValue(Any())
.Throws<InvalidOperationException>()
.Then().Returns("retry-succeeded")
.Then().Returns("cached");
mock.GetValue(Any()).ReturnsSequentially("first", "second", "third");
The last entry repeats indefinitely after the sequence is exhausted.
Property setups
mock.Name.Returns("Alice");
mock.Name.Getter.Returns("Alice");
mock.Count.Setter.Callback(() => Console.WriteLine("Count was set"));
mock.Count.Set(Is(42)).Callback(() => Console.WriteLine("Count set to 42"));
Step 4: Match arguments with Arg
Setups and verifications use argument matchers from the Arg static class. TUnit.Mocks adds using static TUnit.Mocks.Arg as a global using so the matchers are available unqualified:
| Matcher | Meaning |
|---|
Any() or Any<T>() | Any value, including null |
Is<T>(value) | Exact equality |
Is<T>(predicate) | Value satisfies a predicate |
IsNotNull<T>() | Value is not null |
IsIn(a, b, c) | Value is one of the listed |
IsNotIn(a, b, c) | Value is none of the listed |
mock.GetUser(Is(42)).Returns(new User("Alice"));
mock.GetUser(Is<int>(id => id > 0)).Returns(new User("Valid"));
mock.Process(IsNotNull<string>()).Returns("had value");
mock.GetRole(IsIn("admin", "editor", "viewer")).Returns(true);
mock.GetRole(IsNotIn("admin", "superadmin")).Returns(false);
For ref struct parameters on .NET 9+, use RefStructArg<T>.Any instead of Any<T>() โ ref structs cannot be generic type arguments, so the standard matchers don't apply.
Step 5: Verify calls
After exercising the system under test, assert what was called on the mock.
mock.GetUser(42).WasCalled();
mock.GetUser(42).WasCalled(Times.Once);
mock.Delete(Any()).WasNeverCalled();
mock.GetUser(Any()).WasCalled(Times.Exactly(3));
mock.GetUser(id => id > 0).WasCalled(Times.AtLeast(1));
Times constants:
| Constant | Meaning |
|---|
Times.Once | Exactly 1 |
Times.Never | Exactly 0 |
Times.AtLeastOnce | โฅ 1 |
Times.Exactly(n) | Exactly n |
Times.AtLeast(n) | โฅ n |
Times.AtMost(n) | โค n |
Times.Between(min, max) | Inclusive range |
Ordered verification
When the order of calls matters across mocks, wrap the expectations in Mock.VerifyInOrder(...):
Mock.VerifyInOrder(() =>
{
mockLogger.Log("Starting").WasCalled();
mockRepo.SaveAsync(Any()).WasCalled();
mockLogger.Log("Done").WasCalled();
});
If the actual call order differs, the verification fails.
Strict verification
To assert that no calls happened beyond what was explicitly verified:
mock.GetUser(1).WasCalled(Times.Once);
mock.Delete(2).WasCalled(Times.Once);
mock.VerifyNoOtherCalls();
Use this sparingly โ it makes tests brittle to implementation changes. Prefer to verify only what the test actually cares about.
Step 6: Mock HTTP with TUnit.Mocks.Http
Mock.HttpClient(...) returns a real HttpClient backed by an interceptor handler โ drop-in for code that takes an HttpClient:
using var client = Mock.HttpClient("https://example.com");
client.Handler
.OnGet("/api/users")
.RespondWithJson("""[{"id": 1, "name": "Alice"}]""");
client.Handler
.OnPost("/api/users")
.Respond(HttpStatusCode.Created);
client.Handler
.OnGet("/api/version")
.RespondWithString("1.0.0");
Without a base address: Mock.HttpClient(). To get the handler alone (e.g. to inject into new HttpClient(handler)): Mock.HttpHandler().
Response builder โ status, body, headers
client.Handler
.OnGet("/api/data")
.Respond(HttpStatusCode.OK)
.WithJsonContent("""{"key": "value"}""")
.WithHeader("X-Request-Id", "abc123");
Custom request matching
client.Handler.OnRequest(r => r.PathStartsWith("/api/v2"))
.RespondWithJson("""{"version": 2}""");
client.Handler.OnRequest(r => r.PathMatches(@"/api/users/\d+"))
.RespondWithJson("""{"id": 1, "name": "Alice"}""");
client.Handler.OnRequest(r => r.Header("Authorization", "Bearer token"))
.RespondWithJson("""{"user": "admin"}""");
client.Handler.OnRequest(r => r.BodyContains("searchQuery"))
.RespondWithJson("""{"results": []}""");
Sequenced responses
var setup = client.Handler.OnGet("/api/status");
setup.RespondWithString("starting");
setup.Then().RespondWithString("running");
setup.Then().RespondWithString("complete");
Verification
client.Handler.Verify(r => r.Method(HttpMethod.Get).Path("/api/users"), Times.Once);
client.Handler.VerifyNoUnmatchedRequests();
await Assert.That(client.Handler.Requests).Count().IsEqualTo(2);
await Assert.That(client.Handler.Requests[0].Method).IsEqualTo(HttpMethod.Get);
Step 7: Mock ILogger with TUnit.Mocks.Logging
Mock.Logger(...) produces a real ILogger (or ILogger<T>) that records every entry for inspection.
var logger = Mock.Logger();
var logger = Mock.Logger("MyApp.Services");
var logger = Mock.Logger<MyService>();
Asserting log entries
Fluent matcher API:
logger.VerifyLog().AtLevel(LogLevel.Error).WasCalled(Times.Once);
logger.VerifyLog().ContainingMessage("failed").WasCalled();
logger.VerifyLog().WithMessage("Operation completed").WasCalled();
logger.VerifyLog().WithException<InvalidOperationException>().WasCalled(Times.Once);
logger.VerifyLog()
.AtLevel(LogLevel.Error)
.ContainingMessage("timeout")
.WithException<TimeoutException>()
.WasCalled(Times.AtLeastOnce);
Shorthand forms:
logger.VerifyLog(LogLevel.Error, "connection failed");
logger.VerifyLog(LogLevel.Warning, "retry", Times.Exactly(3));
logger.VerifyNoLog(LogLevel.Error);
logger.VerifyNoLogs();
Inspecting captured entries directly
foreach (var entry in logger.Entries)
{
}
var latest = logger.LatestEntry;
await Assert.That(latest.LogLevel).IsEqualTo(LogLevel.Information);
Validation
Common Pitfalls
| Pitfall | Solution |
|---|
TM004 build error on mock generation | Bump <LangVersion> to 14 (or latest) โ TUnit.Mocks requires C# 14. |
Wrapping async return values: Returns(Task.FromResult(value)) | Just write Returns(value) โ TUnit.Mocks auto-wraps to Task<T>/ValueTask<T> based on the method's return type. |
Looking for .Object on the mock | There isn't one. The mock IS the interface. Pass it directly. |
Forgetting Any() in setups: mock.GetUser(42).Returns(...) when you wanted any input | If the test uses a specific id, Is(42) makes intent explicit; if any value should match, use Any(). Setting up with a literal value only matches that exact value. |
WasCalled() (no Times.*) used to assert exactly-once | WasCalled() checks "at least once." Use WasCalled(Times.Once) for exactly one call. |
VerifyNoOtherCalls() on every test, then breaking on every refactor | Use only when the contract really is "these calls and nothing else." Otherwise, verify the calls you care about and let the rest go. |
Mocking HttpClient by subclassing HttpMessageHandler instead of using Mock.HttpClient(...) | Mock.HttpClient(...) gives matching/verification/sequence support out of the box. Hand-rolled handlers re-implement all of that. |
Asserting on log strings via mock.LoggedMessages | Use Mock.Logger() from TUnit.Mocks.Logging. The VerifyLog(...) API is purpose-built for log assertions and survives format-string changes. |
Ref-struct parameters fail to compile with Any<T>() | On .NET 9+, use RefStructArg<T>.Any for ref-struct parameters โ they can't be generic type arguments. |
Ordering verification using a list of WasCalled calls outside VerifyInOrder | Individual WasCalled checks don't enforce order. Wrap the sequence in Mock.VerifyInOrder(() => { ... }). |