Advanced Moq Patterns — Protected Members, Recursive Mocks and Moq.AutoMock

📋 Table of Contents
  1. Advanced Moq Patterns
  2. Common Mistakes

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();
}
Note: Moq uses Castle.DynamicProxy under the hood to create a runtime-generated subclass or interface implementation. This means Moq can only mock: interfaces (always), abstract classes, and non-sealed classes with virtual/abstract members. Sealed classes, static methods, and non-virtual instance methods cannot be mocked by Moq. When you encounter an unmockable dependency, apply the Adapter pattern — wrap the dependency in an interface that you can mock. For example, wrap HttpClient in IHttpClientWrapper with virtual methods.
Tip: Use 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.
Warning: Mocking 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.

🧠 Test Yourself

A PostsService has 6 constructor dependencies. A test only needs to mock 2 of them. With AutoMocker, how many mocks must be manually configured?