Advanced Moq patterns address real-world testing challenges: testing retry logic requires different responses on successive calls (SetupSequence), testing services that use HttpClient requires mocking the message handler rather than the client directly, and testing complex service graphs with many dependencies is simplified with auto-mocking. Understanding Moq’s proxy-based architecture explains its limitations and guides the design of testable code.
Advanced Moq Patterns
// ── SetupSequence — test retry logic ──────────────────────────────────────
[Fact]
public async Task RetryPolicy_OnTransientFailure_RetriesAndSucceeds()
{
var repoMock = new Mock<IPostsRepository>();
repoMock.SetupSequence(r => r.GetByIdAsync(1, It.IsAny<CancellationToken>()))
.ThrowsAsync(new TimeoutException("DB timeout")) // 1st call: fail
.ThrowsAsync(new TimeoutException("DB timeout")) // 2nd call: fail
.ReturnsAsync(new Post { Id = 1, Title = "Success" }); // 3rd: succeed
var result = await _sut.GetByIdWithRetryAsync(1, default);
result.Should().NotBeNull();
result!.Title.Should().Be("Success");
repoMock.Verify(r => r.GetByIdAsync(1, It.IsAny<CancellationToken>()), Times.Exactly(3));
}
// ── Mock HttpClient via HttpMessageHandler ────────────────────────────────
// HttpClient is NOT directly mockable — mock the handler instead
public class EmailServiceTests
{
[Fact]
public async Task SendPasswordReset_WhenSmtpResponds200_ReturnsSuccess()
{
var handlerMock = new Mock<HttpMessageHandler>();
handlerMock.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(@"{""id"":""msg_123""}")
});
var httpClient = new HttpClient(handlerMock.Object)
{
BaseAddress = new Uri("https://api.sendgrid.com")
};
var factoryMock = new Mock<IHttpClientFactory>();
factoryMock.Setup(f => f.CreateClient("SendGrid")).Returns(httpClient);
var sut = new EmailService(factoryMock.Object, /* config */);
var result = await sut.SendPasswordResetAsync("user@example.com",
"reset-token", default);
result.Should().BeTrue();
handlerMock.Protected().Verify(
"SendAsync",
Times.Once(),
ItExpr.Is<HttpRequestMessage>(req =>
req.Method == HttpMethod.Post &&
req.RequestUri!.ToString().Contains("mail/send")),
ItExpr.IsAny<CancellationToken>());
}
}
// ── Moq.AutoMock — auto-create mocks for all dependencies ─────────────────
// Install: dotnet add package Moq.AutoMock
[Fact]
public async Task AutoMock_PostsService_CreateAsync_Works()
{
using var mocker = new AutoMocker();
// Configure only the mocks you care about
mocker.GetMock<IPostsRepository>()
.Setup(r => r.SlugExistsAsync(It.IsAny<string>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
mocker.GetMock<ICurrentUserService>()
.SetupGet(u => u.UserId).Returns("user-123");
// Create the service with ALL mocks automatically injected
var sut = mocker.CreateInstance<PostsService>();
var result = await sut.CreateAsync(
new CreatePostRequest("Title", null, "Body", "draft"), default);
result.Should().NotBeNull();
}
HttpClient in IHttpClientWrapper with virtual methods.Moq.AutoMock (AutoMocker) when a service has many constructor dependencies and most tests only care about one or two of them. Without AutoMock, you write new Mock<A>(), new Mock<B>(), new Mock<C>()... for every test class. With AutoMock: var sut = mocker.CreateInstance<PostsService>() — all dependencies are automatically mocked. Only set up the mocks you need. When a new dependency is added to the service constructor, existing tests continue to work without modification.HttpMessageHandler.SendAsync requires using Protected() and ItExpr (instead of It) because SendAsync is a protected virtual method. This is one of the less elegant parts of Moq’s API. A cleaner alternative: implement a custom MockHttpMessageHandler class that overrides SendAsync directly — this is more readable, more flexible for complex scenarios, and doesn’t require Moq’s Protected() API. Both approaches work; the custom handler is more maintainable for complex HTTP mocking scenarios.Common Mistakes
Mistake 1 — Mocking HttpClient directly (throws MissingMethodException)
❌ Wrong — new Mock<HttpClient>(); HttpClient is not mockable; Moq cannot proxy its non-virtual methods.
✅ Correct — mock HttpMessageHandler via Protected() or implement a custom handler that overrides SendAsync.
Mistake 2 — Using It.IsAny for CancellationToken but ItExpr.IsAny in Protected() (inconsistent API)
❌ Wrong — mixing It.IsAny and ItExpr.IsAny in the same mock setup; Protected() methods require ItExpr.
✅ Correct — use ItExpr.IsAny<T>() (not It.IsAny<T>()) for all parameters in Protected() setups.