The Moq Guide: Setup, SetupSequence, It Matchers, Callback, Verify, Strict Mocks, Protected and Mock.Of — with FakeItEasy Equivalents
In Moq, MockBehavior.Strict and VerifyAll turn every conversation a service has with its collaborators into a contract: even a single log line that changes no behavior breaks the legacy test with a MockException. Setup/ReturnsAsync, SetupSequence, It matchers, out parameters, Callback, Verify and VerifyNoOtherCalls, SetupProperty, As<T>(), Protected() and Mock.Of, with version pinning, an NSubstitute/FakeItEasy mapping table and real failure messages.
Version note: The code in this post was built and run with .NET 10, xUnit.net v3 4.0.3, Moq 4.21.0 (Castle.Core 5.2.1) and FakeItEasy 9.0.1.
Predict First
The series uses NSubstitute, but in a large share of .NET projects the tests are written with Moq, and you have to read and maintain that code. This post goes through Moq's API using an old Ordering test project from Bookstore, the online bookstore used as the shared example across this site's series. The subject is Institutional Orders: bulk orders of at least 20 copies from schools, libraries and companies, with a 10% discount. The project contains a test written years ago:
public sealed class InstitutionalCheckoutLegacyTests
{
private readonly MockRepository mocks = new(MockBehavior.Strict);
[Fact]
public async Task PlaceAsync_ValidInstitutionalOrder_SavesOrder()
{
Mock<IOrderRepository> orders = mocks.Create<IOrderRepository>();
Mock<IStockService> stock = mocks.Create<IStockService>();
Mock<IPriceRules> prices = mocks.Create<IPriceRules>();
Mock<ICheckoutSession> session = mocks.Create<ICheckoutSession>();
Mock<TimeProvider> clock = mocks.Create<TimeProvider>();
Mock<ILogger<InstitutionalCheckout>> logger = mocks.Create<ILogger<InstitutionalCheckout>>();
int available = 100;
stock.Setup(s => s.TryReserve("9780441172719", 25, out available)).Returns(true);
prices.Setup(p => p.DiscountRate(It.IsAny<CustomerAccount>(), 25)).Returns(0.10m);
clock.Setup(c => c.GetUtcNow()).Returns(new DateTimeOffset(2026, 11, 14, 9, 30, 0, TimeSpan.Zero));
orders.Setup(o => o.NextSequence(2026)).Returns(42);
orders.Setup(o => o.SaveAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>())).Returns(Task.CompletedTask);
session.SetupSet(s => s.LastOrderNumber = "BS-2026-000042");
InstitutionalCheckout sut = new(orders.Object, stock.Object, prices.Object, session.Object, clock.Object, logger.Object);
CheckoutReceipt receipt = await sut.PlaceAsync(
new CustomerAccount("library@example.org", true, 3),
[new OrderLine("9780441172719", 25, 12.50m)],
TestContext.Current.CancellationToken);
Assert.Equal("BS-2026-000042", receipt.OrderNumber);
mocks.VerifyAll();
}
}
The support team wanted institutional orders to show up in the logs, and a single line was added at the end of InstitutionalCheckout.PlaceAsync:
logger.LogInformation("Placed institutional order {OrderNumber} with {Quantity} copies", number, quantity);
The behavior did not change: same order, same number, same price. Does the test pass?
The Answer: A Strict Mock Turns Every Conversation into a Contract
It fails:
Moq.MockException : ILogger.Log<FormattedLogValues>(LogLevel.Information, 0, Placed institutional order BS-2026-000042 with 25 copies, null, Func<FormattedLogValues, Exception, string>) invocation failed with mock behavior Strict.
All invocations on the mock must have a corresponding setup.
Every mock created by MockRepository(MockBehavior.Strict) throws on an unconfigured call. LogInformation is an extension method; internally it calls ILogger.Log<TState>, and that call has no setup. The test has turned every conversation the service has with its collaborators into a contract: adding a log line, a property read or another query breaks the test even though the behavior stays the same. The mocks.VerifyAll() at the end does the same thing from the other direction: it requires every setup to be called at least once, so removing a query breaks the test too. Post 08 discussed this as a lack of resistance to refactoring; here we look at which Moq APIs lead to it and how the same test simplifies.
The rules of the simplified version: mocks are loose, which is Moq's default; the logger is a real NullLogger<T>, or FakeLogger from post 11 if the log has to be verified; queries (stock, price rule, clock, sequence number) are only set up, never verified; and verification covers only effects that leave the system, saving the order, and the result. All of the tests below are green with that log line in place.
Setup, Returns and SetupSequence
public sealed class InstitutionalCheckoutTests
{
private const string Dune = "9780441172719";
private const string Hobbit = "9780261103344";
private static readonly CustomerAccount School = new("library@example.org", IsInstitutional: true, CompletedOrders: 3);
private readonly Mock<IOrderRepository> orders = new();
private readonly Mock<IStockService> stock = new();
private readonly Mock<ICheckoutSession> session = new();
private readonly Mock<TimeProvider> clock = new();
private readonly IPriceRules prices = Mock.Of<IPriceRules>(
p => p.DiscountRate(It.Is<CustomerAccount>(c => c.IsInstitutional), It.IsAny<int>()) == 0.10m);
public InstitutionalCheckoutTests()
{
int available = 500;
stock.Setup(s => s.TryReserve(It.IsRegex(@"^97[89]\d{10}$"), It.IsInRange(1, 500, Moq.Range.Inclusive), out available))
.Returns(true);
clock.Setup(c => c.GetUtcNow()).Returns(new DateTimeOffset(2026, 11, 14, 9, 30, 0, TimeSpan.Zero));
orders.Setup(o => o.NextSequence(2026)).Returns(42);
session.SetupProperty(s => s.LastOrderNumber);
}
private InstitutionalCheckout CreateSut() => new(
orders.Object, stock.Object, prices, session.Object, clock.Object, NullLogger<InstitutionalCheckout>.Instance);
private Task<CheckoutReceipt> Place(params OrderLine[] lines) =>
CreateSut().PlaceAsync(School, lines, TestContext.Current.CancellationToken);
[Fact]
public async Task Order_numbers_follow_the_repository_sequence()
{
orders.SetupSequence(o => o.NextSequence(2026))
.Returns(42)
.Returns(43);
CheckoutReceipt first = await Place(new OrderLine(Dune, 20, 12.50m));
CheckoutReceipt second = await Place(new OrderLine(Hobbit, 30, 9.90m));
Assert.Equal("BS-2026-000042", first.OrderNumber);
Assert.Equal("BS-2026-000043", second.OrderNumber);
}
[Fact]
public async Task Save_timeout_propagates_and_session_is_not_updated()
{
orders.Setup(o => o.SaveAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new TimeoutException("Database did not respond in 30s."));
await Assert.ThrowsAsync<TimeoutException>(() => Place(new OrderLine(Dune, 20, 12.50m)));
Assert.Null(session.Object.LastOrderNumber);
}
}
In Moq everything is configured through a Mock<T>, and the code under test receives .Object. Setup(x => x.Method(args)) takes a lambda expression; Moq parses the expression to work out which call is being configured. The return value is given with Returns(value), Returns(() => ...) or Returns((int year) => ...), which receives the arguments; async methods use ReturnsAsync, exceptions use Throws/ThrowsAsync. An unconfigured method returning Task returns a completed Task on a loose mock, which is why we configure SaveAsync only in the exception test.
SetupSequence gives different answers to consecutive calls; Returns, Throws, ReturnsAsync and ThrowsAsync can be chained in any mix, as in the timeout scenario in post 07. When the sequence runs out, Moq returns default(T) instead of repeating the last value; that is a difference from NSubstitute.
Two more details. TimeProvider is not an interface but an abstract class; since GetUtcNow is virtual, Moq can fake it too. For the clock in tests, though, we still recommend FakeTimeProvider from post 11: it can advance time and also drives timers. Mock.Of<T>(predicate) is LINQ to Mocks, Moq's syntax that lets you write a setup like a LINQ query: "give me an IPriceRules whose call returns this", returning a T directly. It is short and readable for query-style dependencies; if you need to verify, Mock.Get(prices) takes you back to the Mock<T>.
It Matchers, out Parameters and Callback
The stock setup in the constructor combines three matchers: It.IsRegex accepts a valid ISBN-13 format and It.IsInRange(1, 500, Moq.Range.Inclusive) a sensible quantity range. Others are It.IsAny<T>(), It.Is<T>(predicate), It.IsIn(values)/It.IsNotIn(values) and It.IsNotNull<T>(). The It.Is<CustomerAccount>(c => c.IsInstitutional) inside Mock.Of defines the discount for institutional customers only; if an individual customer comes in, the setup does not match and the loose mock returns 0m.
In Moq, out parameters are filled with the value of the variable at setup time:
[Fact]
public async Task Short_stock_is_reported_with_the_available_count()
{
int available = 7;
stock.Setup(s => s.TryReserve(Dune, 25, out available)).Returns(false);
InsufficientStockException ex = await Assert.ThrowsAsync<InsufficientStockException>(
() => Place(new OrderLine(Dune, 25, 12.50m)));
Assert.Equal(7, ex.Available);
orders.Verify(o => o.SaveAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>()), Times.Never());
}
The value is read the moment Setup is called; changing available afterwards does not affect the setup. The out parameter takes no part in matching, so this setup matches the "Dune, 25 copies" call. If you need a different out value per argument, write a separate Setup for each case. For matching ref parameters there is It.Ref<T>.IsAny.
To capture an argument, use Callback:
[Fact]
public async Task Bulk_order_is_saved_with_the_institutional_discount()
{
Order? saved = null;
orders.Setup(o => o.SaveAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>()))
.Callback<Order, CancellationToken>((order, _) => saved = order)
.Returns(Task.CompletedTask);
CheckoutReceipt receipt = await Place(new OrderLine(Dune, 20, 12.50m), new OrderLine(Hobbit, 5, 9.90m));
Assert.Equal(new CheckoutReceipt("BS-2026-000042", 269.55m, clock.Object.GetUtcNow()), receipt);
Assert.NotNull(saved);
Assert.Equal([new OrderLine(Dune, 20, 11.25m), new OrderLine(Hobbit, 5, 8.91m)], saved.Lines);
orders.Verify(o => o.SaveAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>()), Times.Once());
}
The generic type parameters of Callback must match the method signature; if they do not, the error comes not at compile time but when the setup line runs (with Callback<Order>: "Invalid callback. Setup on method with parameters (Order, CancellationToken) cannot invoke callback with parameters (Order)."). Checking the captured order with separate assertions gives a more readable failure message than burying the whole check inside It.Is<Order>(...); the next section shows why.
Verify, Times and VerifyNoOtherCalls
Verify(expression, Times.X()) checks how many times a call was made: Times.Once(), Never(), Exactly(n), AtLeastOnce(), AtMost(n), Between(a, b, Range.Inclusive). If we check the order total inside Verify and expect the wrong value:
Moq.MockException :
Expected invocation on the mock once, but was 0 times: o => o.SaveAsync(It.Is<Order>(x => x.Total == 250), It.IsAny<CancellationToken>())
Performed invocations:
Mock<IOrderRepository:8> (o):
IOrderRepository.NextSequence(2026)
IOrderRepository.SaveAsync(Order, CancellationToken)
The message lists the calls that were made, but since Order is not a record it only prints the type name; it does not say what the total was. That is why we preferred capturing with Callback and comparing with Assert.Equal. In post 16 we saw NSubstitute print every field through a record's ToString; in both libraries, a readable ToString on the argument decides the quality of the message.
VerifyNoOtherCalls() requires that no unverified calls remain. stock.VerifyNoOtherCalls() makes sense in the "stock is never touched for fewer than 20 copies" test. Used on the order repository, though:
Moq.MockException : Mock<IOrderRepository:11>:
This mock failed verification due to the following unverified invocations:
IOrderRepository.NextSequence(2026)
NextSequence is a query; it was set up but not Verify'd, so it counts as an "unverified invocation". Adding VerifyNoOtherCalls to every mock does what the strict mock in Predict First did, by another route.
Setups marked with Verifiable() are verified by Verify(), and all setups by VerifyAll(). These two calls, common in old Moq tests, say "everything you set up must be used"; for query setups that is usually the wrong contract.
Properties, As<T>() and Protected()
Moq's default property behavior differs from NSubstitute and FakeItEasy. When I removed the session.SetupProperty(s => s.LastOrderNumber) line, the service set the property but reading it returned null:
Assert.Equal() Failure: Strings differ
Expected: "BS-2026-000042"
Actual: null
A loose Moq mock does not store assignments to a property; SetupProperty makes that property behave like a real auto-property, and SetupAllProperties() does the same for all properties. In NSubstitute and FakeItEasy an assigned value is read back by default; our FakeItEasy test needed no extra setup.
As<T>() adds a second interface to a mock. If the repository also implements IUnitOfWork, the service commits after saving:
[Fact]
public async Task Unit_of_work_is_committed_after_the_save()
{
List<string> calls = [];
orders.Setup(o => o.SaveAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>()))
.Callback(() => calls.Add("save"))
.Returns(Task.CompletedTask);
orders.As<IUnitOfWork>()
.Setup(u => u.CommitAsync(It.IsAny<CancellationToken>()))
.Callback(() => calls.Add("commit"))
.Returns(Task.CompletedTask);
await Place(new OrderLine(Dune, 20, 12.50m));
Assert.Equal(["save", "commit"], calls);
}
orders.Object is now both an IOrderRepository and an IUnitOfWork. As<T>() must be called before the mock's .Object is used for the first time; calling it afterwards throws "Mock type has already been initialized by accessing its Object property. Adding interfaces must be done before that." Writing the order of calls into a list with Callback and checking it with a single Assert.Equal reads better than MockSequence, which was designed for strict mocks.
Protected() lets you configure protected members. Let us test an export job written with the template method pattern without writing a subclass:
[Fact]
public async Task Export_job_publishes_every_pending_order()
{
IReadOnlyList<Order> pending =
[
new(Guid.NewGuid(), "BS-2026-000042", "library@example.org", [new OrderLine("9780441172719", 20, 11.25m)]),
new(Guid.NewGuid(), "BS-2026-000043", "school@example.org", [new OrderLine("9780261103344", 30, 8.91m)]),
];
Mock<OrderExportJob> job = new();
job.Protected()
.Setup<Task<IReadOnlyList<Order>>>("LoadPendingAsync", ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(pending);
job.Protected()
.Setup<Task>("PublishAsync", ItExpr.IsAny<Order>(), ItExpr.IsAny<CancellationToken>())
.Returns(Task.CompletedTask);
int published = await job.Object.RunAsync(TestContext.Current.CancellationToken);
Assert.Equal(2, published);
job.Protected().Verify(
"PublishAsync", Times.Exactly(2), ItExpr.IsAny<Order>(), ItExpr.IsAny<CancellationToken>());
}
Protected members cannot be written as lambdas, so the name is passed as a string and the matchers come from ItExpr instead of It. When I wrote It.IsAny out of habit, the test failed with a misleading message: ArgumentException : Use ItExpr.IsNull<TValue> rather than a null argument value, as it prevents proper method lookup. In this context It.IsAny returns null, so Moq sees it as a null argument. A refactoring that renames the method breaks this test at run time, not at compile time. So use Protected() to bring an existing hierarchy under test; in new code, an injected interface is easier to test than a protected abstract method.
Living with Existing Moq Code: Version Pinning and a Mapping Table
Post 07 covered Moq's SponsorLink incident in 2023. The state of NuGet on the day of writing: 4.20.0, 4.20.1 and 4.20.2 are unlisted; they do not show up in search, but a project that asks for the exact version can still download them. If you see one of these versions in an old project, do not postpone the upgrade. 4.20.72 (September 2024) was the latest release for a long time; 4.21.0 arrived on 24 September 2026 and, according to its release notes, compiles an expression-based It.Is only once, supports nullable constructor arguments and improves performance when mocking many large classes. The licence is BSD-3-Clause and the dependency is Castle.Core 5.2.1 under Apache-2.0. In a project that keeps using Moq, pin the exact version with central package management:
<PackageVersion Include="Moq" Version="[4.21.0]" />
The square brackets mean "exactly this version"; automated dependency updaters open the upgrade as a separate pull request, and you cannot merge it without reading the release notes.
Rewriting tests just to rewrite them is rarely worth it. But if a test is changing anyway, or the team decided to move to NSubstitute, the mapping is mechanical:
| Task | Moq 4.21 | NSubstitute 6.2 | FakeItEasy 9.0 |
|---|---|---|---|
| Create | new Mock<T>() + .Object | Substitute.For<T>() | A.Fake<T>() |
| Return | Setup(x => x.M(a)).Returns(v) | x.M(a).Returns(v) | A.CallTo(() => x.M(a)).Returns(v) |
| Async return | ReturnsAsync(v) | Returns(v) | Returns(v) |
| Sequential returns | SetupSequence(...).Returns(a).Returns(b) | Returns(a, b) | ReturnsNextFromSequence(a, b) |
| Exception | Throws / ThrowsAsync | Throws / ThrowsAsync | Throws / ThrowsAsync |
| Any value | It.IsAny<T>() | Arg.Any<T>() | A<T>._ |
| Conditional | It.Is<T>(p) | Arg.Is<T>(p) | A<T>.That.Matches(p) |
| out parameter | variable at setup | call[i] = v | AssignsOutAndRefParameters(v) |
| Capture argument | Callback(...) | Arg.Do / When..Do | Invokes(...) |
| Called once | Verify(..., Times.Once()) | Received(1) | MustHaveHappenedOnceExactly() |
| Never called | Times.Never() | DidNotReceive() | MustNotHaveHappened() |
| Property | SetupProperty required | automatic | automatic |
| Strict | MockBehavior.Strict | none | A.Fake<T>(o => o.Strict()) |
The same institutional order scenario with FakeItEasy:
public InstitutionalCheckoutFakeTests()
{
int available;
A.CallTo(() => stock.TryReserve(A<string>._, A<int>._, out available))
.Returns(true)
.AssignsOutAndRefParameters(500);
A.CallTo(() => prices.DiscountRate(A<CustomerAccount>.That.Matches(c => c.IsInstitutional), A<int>._))
.Returns(0.10m);
A.CallTo(() => clock.GetUtcNow()).Returns(new DateTimeOffset(2026, 11, 14, 9, 30, 0, TimeSpan.Zero));
A.CallTo(() => orders.NextSequence(2026)).ReturnsNextFromSequence(42, 43);
}
[Fact]
public async Task Bulk_order_is_saved_once_with_the_discount()
{
await Place(new OrderLine(Dune, 20, 12.50m));
A.CallTo(() => orders.SaveAsync(A<Order>.That.Matches(o => o.Total == 225.00m), A<CancellationToken>._))
.MustHaveHappenedOnceExactly();
Assert.Equal("BS-2026-000042", session.LastOrderNumber);
}
FakeItEasy has no .Object: A.Fake<T>() returns a T directly and every setup starts with A.CallTo. The out value is given after the setup with AssignsOutAndRefParameters, and session.LastOrderNumber returns the assigned value without any setup. All three libraries use the same Castle.Core proxy at run time; Moq 4.21.0 and FakeItEasy 9.0.1 depend on Castle.Core 5.2.1, NSubstitute 6.2.0 on 5.1.1. FakeItEasy is licensed under MIT.
In Production
Moq's failure exceptions do not show up as assertion failures in xUnit v3. The output reports them like an unexpected exception, as Xunit.MicrosoftTestingPlatform.XunitException: Moq.MockException : ... (see the IAssertionException section in post 14); NSubstitute's ReceivedCallsException behaves the same. If you use a CI report that groups test results by failure type, expect mock verifications to appear under "exception".
Do not simplify legacy tests in one go. If a test project has hundreds of strict mocks, first switch the MockRepository behavior to loose and examine the tests that turn red one by one: a test that fails under loose may show that a behavior really changed. Then replace the VerifyAll calls with Verify calls that check only the calls with an effect outside the system.
Look at the number of constructor dependencies. InstitutionalCheckout takes six dependencies, and half of the legacy test's setup is about them. That is a design problem, not a test problem; splitting the service, for example moving stock reservation into a separate domain service, belongs to the Refactoring series.
Common Pitfalls
Locking down the implementation with VerifyAll in every test. Verifying query setups is the Moq form of the "asserting on a stub" mistake from post 06: the test breaks when the service starts producing the same result with fewer queries.
A strict mock breaking every test on a small refactoring. The log line in Predict First breaks not one test but every test of that service that uses strict mocks. Keep strict behavior only where you really mean "this call must never happen", and even there Times.Never() is usually enough.
Verifying ILogger with Moq. Writing logger.Verify(l => l.LogInformation(...)) fails the test with NotSupportedException : Unsupported expression: l => l.LogInformation(It.IsAny<string>(), It.IsAny<object[]>()) Extension methods (here: LoggerExtensions.LogInformation) may not be used in setup / verification expressions. Extension methods cannot be mocked; verifying ILogger.Log<TState> directly is possible but hard to read. If you need to verify logging, use FakeLogger.
Having Moq in the test project's namespace. I first named this post's test project Bookstore.Ordering.Moq.Tests, and the build stopped with error CS0234: The type or namespace name 'Range' does not exist in the namespace 'Bookstore.Ordering.Moq': inside the namespace, Moq.Range pointed at our own namespace instead of the library. I renamed the project to Bookstore.Ordering.Legacy.Tests; the same problem applies to FakeItEasy.
A Small Experiment
★☆☆ Add the legacy test from Predict First to your project and change MockBehavior.Strict to MockBehavior.Loose. Does the test pass? Then, without deleting mocks.VerifyAll(), remove the prices.DiscountRate call from InstitutionalCheckout by making the discount a fixed 0.10m, and run it: what happened?
★★☆ Extend Short_stock_is_reported_with_the_available_count to a two-line order: Dune is in stock, The Hobbit is short. Write a Setup with a separate out value for the second line and verify that nothing was saved.
★★★ Move InstitutionalCheckoutTests to NSubstitute. How many lines changed with the mapping in the table, and in which test was the mapping not enough (SetupProperty, As<T>(), Mock.Of and the like)?
In the next post we unit test code that uses HttpClient: an HttpMessageHandler stub, RichardSzalay.MockHttp, IHttpClientFactory and typed clients, through the Shipping module's parcel tracking client.