Moq
unit testing
C#
mocking
void method

Mock an update method returning a void with Moq

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When a method returns void, there is no value to configure with .Returns(). In Moq, you test a void method by verifying that it was called, by capturing its arguments with .Callback(), or by configuring it to throw so you can exercise error paths.

Start with Verification

Suppose you have a repository with a void update method:

csharp
1public interface IUserRepository
2{
3    void Update(int userId, string name);
4    void Save();
5}

A basic test usually does not need a return setup at all:

csharp
1using Moq;
2using Xunit;
3
4public class UserServiceTests
5{
6    [Fact]
7    public void UpdateUser_CallsRepositoryUpdate()
8    {
9        var mockRepo = new Mock<IUserRepository>();
10        mockRepo.Setup(r => r.Update(It.IsAny<int>(), It.IsAny<string>()));
11
12        var service = new UserService(mockRepo.Object);
13
14        service.UpdateUserName(1, "Alice");
15
16        mockRepo.Verify(r => r.Update(1, "Alice"), Times.Once);
17    }
18}

For void methods, verification is usually the core assertion.

Use .Callback() to Capture Arguments

If you want to inspect what was passed to the void method, use .Callback():

csharp
1[Fact]
2public void UpdateUser_CapturesArguments()
3{
4    var mockRepo = new Mock<IUserRepository>();
5
6    int capturedId = 0;
7    string capturedName = "";
8
9    mockRepo.Setup(r => r.Update(It.IsAny<int>(), It.IsAny<string>()))
10        .Callback<int, string>((id, name) =>
11        {
12            capturedId = id;
13            capturedName = name;
14        });
15
16    var service = new UserService(mockRepo.Object);
17    service.UpdateUserName(42, "Bob");
18
19    Assert.Equal(42, capturedId);
20    Assert.Equal("Bob", capturedName);
21}

This is useful when the argument values are part of the behavior under test.

Simulate Errors with .Throws()

Void methods can still throw exceptions, and tests often need to simulate that:

csharp
1[Fact]
2public void UpdateUser_ThrowsWhenRepositoryFails()
3{
4    var mockRepo = new Mock<IUserRepository>();
5
6    mockRepo.Setup(r => r.Update(It.IsAny<int>(), It.IsAny<string>()))
7        .Throws(new InvalidOperationException("Database problem"));
8
9    var service = new UserService(mockRepo.Object);
10
11    Assert.Throws<InvalidOperationException>(
12        () => service.UpdateUserName(1, "Alice")
13    );
14}

This is how you test the service's error path even though the mocked method itself has no return value.

Verify Call Counts

If the important behavior is how often the void method was called, verify that explicitly:

csharp
1[Fact]
2public void BatchUpdate_CallsUpdateForEachUser()
3{
4    var mockRepo = new Mock<IUserRepository>();
5    mockRepo.Setup(r => r.Update(It.IsAny<int>(), It.IsAny<string>()));
6    mockRepo.Setup(r => r.Save());
7
8    var service = new UserService(mockRepo.Object);
9    service.BatchUpdateNames(new Dictionary<int, string>
10    {
11        { 1, "Alice" },
12        { 2, "Bob" },
13        { 3, "Charlie" }
14    });
15
16    mockRepo.Verify(r => r.Update(It.IsAny<int>(), It.IsAny<string>()), Times.Exactly(3));
17    mockRepo.Verify(r => r.Save(), Times.Once);
18}

Being explicit about Times.Once, Times.Never, or Times.Exactly(n) makes the test intention clearer.

Strict vs Loose Mocks

By default, Moq uses loose behavior. That means an unconfigured void method call usually succeeds silently. If you switch to MockBehavior.Strict, unconfigured calls throw MockException.

That choice affects how much setup you need:

  • loose mocks are more forgiving
  • strict mocks make unexpected calls visible immediately

Void methods are often where this difference becomes very noticeable.

Async Is Different: Task Is Not void

If the method returns Task, it is not a void method anymore. Then you do use .Returns(...):

csharp
1public interface IUserRepository
2{
3    Task UpdateAsync(int userId, string name);
4}
5
6[Fact]
7public async Task UpdateUserAsync_CallsRepository()
8{
9    var mockRepo = new Mock<IUserRepository>();
10
11    mockRepo.Setup(r => r.UpdateAsync(It.IsAny<int>(), It.IsAny<string>()))
12        .Returns(Task.CompletedTask);
13
14    var service = new UserService(mockRepo.Object);
15    await service.UpdateUserNameAsync(1, "Alice");
16
17    mockRepo.Verify(r => r.UpdateAsync(1, "Alice"), Times.Once);
18}

That difference matters because many people say "async void-like" when the method actually returns Task.

Common Pitfalls

  • Trying to use .Returns() on a true void method.
  • Forgetting to verify the call after the action under test runs.
  • Relying on loose mocks and assuming a void method was called without checking.
  • Using .Callback() with argument types that do not match the mocked signature.
  • Confusing a Task-returning method with a real void method.

Summary

  • Void methods are tested through verification, callbacks, and exception simulation.
  • Use .Verify() to assert that the method was called with the expected arguments and count.
  • Use .Callback() when you need to inspect the passed values.
  • Use .Throws() to simulate failure paths.
  • If the method returns Task, treat it as async work and configure it with .Returns(Task.CompletedTask).

Course illustration
Course illustration

All Rights Reserved.