Skip to main content

Moq in C# Tutorial: Setup, Verify and Common Errors

Last updated: October 19, 2026 · Tested with Moq 4.21.0 and 4.20.72, xUnit 2.9.3, .NET 10 (SDK 10.0.401, runtime 10.0.12), Windows 11

Short answer: Create a mock with new Mock<IMyService>(), tell it what to return with Setup(...).Returns(...) (or ReturnsAsync for async methods), pass mock.Object into the class you test, and check the calls afterwards with Verify(..., Times.Once). Below is a complete xUnit test suite I ran on Moq 4.21.0, the first Moq release in two years (September 24, 2026), plus the exact exception text for the ten mistakes people hit most. Things I found while testing:

  • Upgrading to Moq 4.21.0 added a compiler warning (CS8602) to the usual ILogger verify code. With warnings as errors, the build fails. On 4.20.72 the same code had 0 warnings.
  • You can't set up or verify logger.LogInformation(...). It's an extension method, so you verify Log(...) instead.
  • An async method you didn't set up doesn't return null. It returns a completed Task whose result is null, so the NullReferenceException appears in your code, not in Moq.
  • HttpClient.GetStringAsync can't be mocked. Mock the HttpMessageHandler instead.

Install Moq

dotnet new xunit -n MoqDemo.Tests
dotnet add MoqDemo.Tests package Moq --version 4.21.0
dotnet add MoqDemo.Tests reference MoqDemo/MoqDemo.csproj

The .NET 10 xunit template gave me xUnit 2.9.3, xunit.runner.visualstudio 3.1.4 and Microsoft.NET.Test.Sdk 17.14.1. Moq works the same with xUnit v3, NUnit and MSTest; only the attribute names and assertions change.

Which Moq version? Use 4.21.0 or 4.20.72. Avoid 4.20.0 and 4.20.1: they shipped with a dependency on Devlooped.SponsorLink, which 4.20.2 removed a day later (August 9, 2023), after build problems and privacy complaints about the e-mail hashing. Both versions are now unlisted on NuGet. 4.21.0 has only Castle.Core and System.Threading.Tasks.Extensions as dependencies.

The Code Under Test

An order service with three interfaces and a logger. Everything it depends on is an interface, which is what makes it easy to mock:

public record Order(int Id, string CustomerEmail, decimal Total);

public interface IOrderRepository
{
    Task<Order?> GetAsync(int id, CancellationToken ct = default);
    Task SaveAsync(Order order, CancellationToken ct = default);
    int Count { get; }
}

public interface IEmailSender
{
    Task SendAsync(string to, string subject, string body);
}

public interface IClock
{
    DateTimeOffset UtcNow { get; }
}

public class OrderService(IOrderRepository repo, IEmailSender email, IClock clock, ILogger<OrderService> logger)
{
    public async Task<bool> ConfirmAsync(int orderId, CancellationToken ct = default)
    {
        var order = await repo.GetAsync(orderId, ct);
        if (order is null)
        {
            logger.LogWarning("Order {OrderId} not found", orderId);
            return false;
        }

        await repo.SaveAsync(order with { Total = Math.Round(order.Total, 2) }, ct);
        await email.SendAsync(order.CustomerEmail, $"Order {order.Id} confirmed",
            $"Confirmed at {clock.UtcNow:O}");
        logger.LogInformation("Order {OrderId} confirmed", order.Id);
        return true;
    }

    public async Task<decimal> TotalAsync(int orderId)
    {
        var order = await repo.GetAsync(orderId);
        return order!.Total;   // no null check, on purpose (see "Loose vs Strict")
    }
}

Your First Moq Test: Setup, ReturnsAsync, Verify

public class OrderServiceTests
{
    private readonly Mock<IOrderRepository> _repo = new();
    private readonly Mock<IEmailSender> _email = new();
    private readonly Mock<IClock> _clock = new();
    private readonly Mock<ILogger<OrderService>> _logger = new();

    private OrderService CreateSut() => new(_repo.Object, _email.Object, _clock.Object, _logger.Object);

    [Fact]
    public async Task ConfirmAsync_saves_and_emails_the_customer()
    {
        _repo.Setup(r => r.GetAsync(7, It.IsAny<CancellationToken>()))
             .ReturnsAsync(new Order(7, "[email protected]", 19.999m));
        _clock.Setup(c => c.UtcNow).Returns(new DateTimeOffset(2026, 10, 19, 9, 0, 0, TimeSpan.Zero));

        var ok = await CreateSut().ConfirmAsync(7);

        Assert.True(ok);
        _repo.Verify(r => r.SaveAsync(It.Is<Order>(o => o.Id == 7 && o.Total == 20.00m),
                                      It.IsAny<CancellationToken>()), Times.Once);
        _email.Verify(e => e.SendAsync("[email protected]", "Order 7 confirmed",
                                       It.Is<string>(b => b.Contains("2026-10-19T09:00:00"))), Times.Once);
    }
}

xUnit creates a new instance of the test class for every test, so the mocks in fields start clean each time. You don't need a reset method.

The three ways to match an argument:

  • A literal value (7, "[email protected]") matches with Equals. Records compare by value, classes by reference.
  • It.IsAny<T>() matches anything. Use it for the CancellationToken, or a setup for GetAsync(7, default) won't match a call that passes a real token.
  • It.Is<T>(predicate) matches when the predicate is true. Moq 4.21.0 compiles the predicate only once, a performance fix listed in its release notes.

Callback: Capture What Was Passed

When a Verify predicate gets long, capture the argument and use normal assertions instead:

[Fact]
public async Task Callback_captures_the_saved_order()
{
    Order? saved = null;
    _repo.Setup(r => r.GetAsync(1, It.IsAny<CancellationToken>()))
         .ReturnsAsync(new Order(1, "[email protected]", 10.005m));
    _repo.Setup(r => r.SaveAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>()))
         .Callback<Order, CancellationToken>((o, _) => saved = o)
         .Returns(Task.CompletedTask);

    await CreateSut().ConfirmAsync(1);

    Assert.NotNull(saved);
    Assert.Equal(10.00m, saved.Total); // Math.Round uses banker's rounding: 10.005 -> 10.00
}

This test caught a real bug. I first expected 10.01m, but Math.Round uses banker's rounding (to even) by default, so 10.005 becomes 10.00. For money you usually want Math.Round(x, 2, MidpointRounding.AwayFromZero).

The callback's type parameters must match the method's parameters exactly, including the CancellationToken. See error 7 below.

ThrowsAsync, SetupSequence and Mock.Of

[Fact]
public async Task ThrowsAsync_lets_you_test_the_failure_path()
{
    _repo.Setup(r => r.GetAsync(5, It.IsAny<CancellationToken>()))
         .ThrowsAsync(new TimeoutException("db timeout"));

    var ex = await Assert.ThrowsAsync<TimeoutException>(() => CreateSut().ConfirmAsync(5));
    Assert.Equal("db timeout", ex.Message);
}

[Fact]
public void SetupSequence_returns_values_in_order()
{
    _repo.SetupSequence(r => r.Count).Returns(1).Returns(2).Throws<InvalidOperationException>();

    Assert.Equal(1, _repo.Object.Count);
    Assert.Equal(2, _repo.Object.Count);
    Assert.Throws<InvalidOperationException>(() => _repo.Object.Count);
}

[Fact]
public void MockOf_builds_a_stub_in_one_line()
{
    var clock = Mock.Of<IClock>(c => c.UtcNow == DateTimeOffset.UnixEpoch);
    Assert.Equal(DateTimeOffset.UnixEpoch, clock.UtcNow);
}

Mock.Of<T> returns the object directly, not a Mock<T>. Use it for stubs you never verify, like a clock. If you need to verify it later, Mock.Get(clock) gives you the mock back.

Loose vs Strict Mocks (and the Hidden null)

new Mock<T>() is loose: a call without a setup returns a default value. For async methods that default is a completed task, but the result inside it is still default(T):

[Fact]
public async Task Unsetup_async_method_on_loose_mock_returns_null_result()
{
    var task = _repo.Object.GetAsync(1);
    Assert.NotNull(task);              // the Task itself is not null...
    Assert.Null(await task);           // ...but its result is default(Order) = null

    await Assert.ThrowsAsync<NullReferenceException>(() => CreateSut().TotalAsync(1));
}

If your test fails with a NullReferenceException deep inside your own code, a missing or non-matching setup is the first thing to check. A strict mock makes that mistake loud instead:

var repo = new Mock<IOrderRepository>(MockBehavior.Strict);
await repo.Object.GetAsync(1);
Moq.MockException: IOrderRepository.GetAsync(1, CancellationToken) invocation failed with mock behavior Strict.
All invocations on the mock must have a corresponding setup.

I use loose mocks by default and strict ones only where an unexpected call would be a bug (payments, deletes). Strict mocks break on every harmless refactoring, like an extra logging call.

VerifyNoOtherCalls

[Fact]
public async Task VerifyNoOtherCalls_passes_when_every_call_is_verified()
{
    _repo.Setup(r => r.GetAsync(It.IsAny<int>(), It.IsAny<CancellationToken>())).ReturnsAsync((Order?)null);

    await CreateSut().ConfirmAsync(42);

    _repo.Verify(r => r.GetAsync(42, It.IsAny<CancellationToken>()), Times.Once);
    _repo.VerifyNoOtherCalls();
    _email.VerifyNoOtherCalls();
}

If any call wasn't verified, Moq lists it:

Moq.MockException: Mock<IOrderRepository:11>:
This mock failed verification due to the following unverified invocations:
   IOrderRepository.Count

How to Verify ILogger with Moq

This is the question I see most. The obvious code doesn't work:

_logger.Verify(l => l.LogInformation("Order {OrderId} confirmed", 3));
System.NotSupportedException: Unsupported expression: l => l.LogInformation("Order {OrderId} confirmed", new[] { (object)3 })
Extension methods (here: LoggerExtensions.LogInformation) may not be used in setup / verification expressions.

LogInformation, LogWarning and the rest are extension methods that all call the one interface method, ILogger.Log<TState>. Verify that, with It.IsAnyType for the generic state:

_logger.Verify(l => l.Log(
        LogLevel.Information,
        It.IsAny<EventId>(),
        It.Is<It.IsAnyType>((state, _) => state != null && state.ToString() == "Order 3 confirmed"),
        It.IsAny<Exception?>(),
        It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
    Times.Once);

The state's ToString() returns the formatted message ("Order 3 confirmed", not the template). The state != null check is new for Moq 4.21.0. The version most blog posts show, (state, _) => state.ToString() == "...", built with 0 warnings on Moq 4.20.72. On 4.21.0 the same line gives:

OrderServiceTests.cs(95,51): warning CS8602: Dereference of a possibly null reference.

With <TreatWarningsAsErrors> (or -warnaserror) that's error CS8602 and the test project stops building. The cause is that 4.21.0 turned on nullable annotations in Moq itself. Don't reach for state?.ToString(): the lambda is an expression tree, and that gives error CS8072: An expression tree lambda may not contain a null propagating operator. An explicit null check works on both versions.

With the null check, all 25 tests built with 0 warnings under -warnaserror and passed on both 4.20.72 and 4.21.0. All the error messages in this post were identical on the two versions.

How to Mock HttpClient with Moq

var http = new Mock<HttpClient>();
http.Setup(h => h.GetStringAsync("x")).ReturnsAsync("y");
System.NotSupportedException: Unsupported expression: h => h.GetStringAsync("x")
Non-overridable members (here: HttpClient.GetStringAsync) may not be used in setup / verification expressions.

Every HttpClient call ends in the handler's protected SendAsync, so mock that with Protected(). Note ItExpr instead of It for protected members:

using Moq.Protected;

[Fact]
public async Task HttpClient_is_tested_by_mocking_HttpMessageHandler()
{
    var handler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
    handler.Protected()
        .Setup<Task<HttpResponseMessage>>("SendAsync",
            ItExpr.Is<HttpRequestMessage>(r => r.RequestUri!.AbsolutePath == "/prices/KB-1"),
            ItExpr.IsAny<CancellationToken>())
        .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent("""{"sku":"KB-1","price":49.90}""", Encoding.UTF8, "application/json")
        });

    var client = new PriceClient(new HttpClient(handler.Object) { BaseAddress = new Uri("https://api.test/") });

    Assert.Equal(49.90m, await client.GetPriceAsync("KB-1"));
    handler.Protected().Verify("SendAsync", Times.Once(),
        ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
}

PriceClient just calls http.GetFromJsonAsync<PriceDto>($"prices/{sku}"). The handler is strict, so a request to any other URL fails with a message that shows the real URL: Moq.MockException: HttpMessageHandler.SendAsync(Method: GET, RequestUri: 'https://api.test/prices/XX-9', ...) invocation failed with mock behavior Strict. With a loose handler, the same mistake only gives InvalidOperationException: Handler did not return a response message., which doesn't tell you which request missed. For how to register the real client, see my HttpClient and IHttpClientFactory guide.

Mocking Classes Instead of Interfaces

Moq creates a subclass at runtime (with Castle DynamicProxy), so it can only replace members it can override:

public class TaxCalculator
{
    public decimal Rate(string country) => country == "DE" ? 0.19m : 0.0m;
    public virtual decimal VirtualRate(string country) => Rate(country);
}

var calc = new Mock<TaxCalculator>();
calc.Setup(c => c.VirtualRate("FR")).Returns(0.2m);   // works: virtual
calc.Object.VirtualRate("US");                         // 0.0, no setup on a loose mock

var real = new Mock<TaxCalculator> { CallBase = true };
real.Object.VirtualRate("DE");                         // 0.19, runs the real code

A setup on the non-virtual Rate, a sealed class or a class with only constructor parameters fails. The exact messages are in the table below. If you control the code, extract an interface. If you don't, wrap the class in a small interface of your own.

Moq Error Messages and Fixes

All ten were produced by tests in my project (Record.Exception, message printed as-is) on Moq 4.21.0, and they were identical on 4.20.72:

Exact message (start)CauseFix
NotSupportedException: Unsupported expression: c => c.Rate("DE") Non-overridable members (here: TaxCalculator.Rate) may not be used in setup / verification expressions.Setup on a non-virtual method of a classMock an interface, or make the member virtual
NotSupportedException: Type to mock (SealedTaxCalculator) must be an interface, a delegate, or a non-sealed, non-static class.Mocking a sealed or static classWrap it behind an interface
NotSupportedException: ... Extension methods (here: LoggerExtensions.LogInformation) may not be used in setup / verification expressions.Verifying LogInformation and similar methodsVerify ILogger.Log with It.IsAnyType (above)
MockException: Expected invocation on the mock once, but was 0 times: e => e.SendAsync("[email protected]", ...)
followed by No invocations performed.
The method was never calledCheck the code path, or that the SUT got mock.Object and not a different instance
Same message, but the list shows IEmailSender.SendAsync("[email protected]", ...)Called, but with other argumentsRead the "Performed invocations" list: it shows the real arguments
MockException: IOrderRepository.GetAsync(1, CancellationToken) invocation failed with mock behavior Strict. All invocations on the mock must have a corresponding setup.Strict mock, call without a matching setupAdd the setup, or use a loose mock
ArgumentException: Invalid callback. Setup on method with parameters (int, CancellationToken) cannot invoke callback with parameters (int).Callback types don't match the method.Callback<int, CancellationToken>((id, _) => ...)
MockException: Mock<IOrderRepository:11>: This mock failed verification due to the following unverified invocations: IOrderRepository.CountVerifyNoOtherCalls found an extra callVerify it too, or drop VerifyNoOtherCalls
ArgumentException: Can not instantiate proxy of class: MoqDemo.OrderService. Could not find a parameterless constructor. (Parameter 'constructorArguments')Mocking a class whose only constructor takes parametersnew Mock<T>(arg1, arg2), or don't mock the class under test at all
MockException: Expected invocation on the mock once, but was 2 times: ...Called more often than Times.OnceFix the double call, or use Times.Exactly(2) / Times.AtLeastOnce()

Moq 4.20.72 vs 4.21.0: What Changed for Me

  • Same 25 tests, same results, same exception texts.
  • One new nullable warning (CS8602) in the ILogger verify predicate, fixed with a null check.
  • The release notes also list faster It.Is matching, faster mocking of many large classes, nullable-friendly constructor arguments, and a fix for issue #1648: .NET Framework 4.8 test projects failing to load System.Threading.Tasks.Extensions after upgrading to 4.20.72. I only tested on .NET 10, so I haven't reproduced that one.

FAQ

Should I mock the DbContext or the repository? Mock the repository interface in service tests. For the repository itself, test against a real database (SQLite in-memory is enough for most queries). In my repository pattern test, two mock-based tests passed while a SQLite test found a soft-delete bug.

Moq or NSubstitute? Both cover the same cases. Moq makes the mock and the object separate (mock.Object). NSubstitute returns the object directly and has a shorter syntax. Pick one per solution. Moq is still maintained: 4.21.0 shipped on September 24, 2026.

How do I return different values on each call? SetupSequence (above), or Returns(() => counter++) when the value is computed.

Where does TDD fit in? Moq is a tool; TDD is the workflow. My TDD with xUnit and Moq guide walks through red-green-refactor with the same packages.

References

Comments

Popular posts from this blog

.NET MAUI Tutorial 2026: Build Cross-Platform Apps in C#

Learn .NET MAUI in 2026 to build iOS, Android, Windows & Mac apps from one C# codebase. Start this cross-platform tutorial with code examples today. .NET MAUI (Multi-platform App UI) is Microsoft's framework for building native iOS, Android, Windows, and macOS apps from a single C# codebase . If you've ever wanted to ship a mobile app without learning Swift, Kotlin, and Win32 separately, this .NET MAUI tutorial for 2026 is your starting point. In this guide you'll learn what .NET MAUI is, why it matters for cross-platform app development in C#, and how to build your first working app — with runnable code examples and the best practices senior engineers actually use in production. What Is .NET MAUI and Why Use It in 2026? .NET MAUI is the evolution of Xamarin.Forms, fully integrated into the modern .NET runtime. With one project and one language — C# — you target four platforms. The framework compiles to native UI controls on each device, so a button on iOS...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...