Unit Testing with Moq: Simulating Different Return Values on Multiple Method Calls

Dec 05, 2025 · Programming · 10 views · 7.8

Keywords: Moq | Unit Testing | SetupSequence

Abstract: This article explores solutions for simulating different return values on multiple method calls in C# unit tests using the Moq framework. Through a concrete case study, it demonstrates how to use the SetupSequence method or custom extension methods like ReturnsInOrder to return values in a specified order, enabling precise control over test scenarios. The article details the implementation principles, applicable contexts, and best practices of these techniques, providing complete code examples and considerations to help developers write more robust and maintainable unit tests.

Problem Background and Scenario Analysis

In unit testing, it is often necessary to simulate the behavior of dependency object methods. Moq, as a widely used mocking framework in the .NET ecosystem, provides rich APIs to configure mock object behaviors. However, when the code under test calls the same mocked method multiple times and requires different return values for each call, the standard Setup method falls short. For example, when testing the ResolvePath method of the DashboardPathResolver class, the GetPageByUrl<IPageModel> method is called twice: the first time should return null, and the second time should return a valid pageModel.Object. This scenario is common in business contexts such as retry logic, caching mechanisms, or stepwise processing.

Core Solution: The SetupSequence Method

The Moq framework introduced the SetupSequence method starting from version 4.2, specifically designed to handle the need for returning different values in a specified call order. This method allows chaining multiple Returns or Throws calls, each corresponding to one execution of the mocked method. Here is a basic example:

repository.SetupSequence(x => x.GetPageByUrl<IPageModel>(virtualUrl))
    .Returns(null)
    .Returns(pageModel.Object);

In this configuration, the first call to GetPageByUrl<IPageModel> returns null, and the second call returns pageModel.Object. If the method is called more than twice, subsequent calls will return the last configured value (i.e., pageModel.Object). This approach is concise and straightforward, requiring no additional dependencies, making it the preferred solution for handling sequential return values.

Alternative Approach: Custom Extension Method ReturnsInOrder

In some cases, developers may be using older versions of Moq or require more flexible configuration. A custom extension method can be implemented to achieve similar functionality. Referencing a solution proposed by Phil Haack in 2009, a ReturnsInOrder extension method can be created:

public static class MoqExtensions
{
    public static IReturnsResult<TMock> ReturnsInOrder<TMock, TResult>(this ISetup<TMock, TResult> setup, params TResult[] results) where TMock : class
    {
        var queue = new Queue<TResult>(results);
        return setup.Returns(() => queue.Count > 0 ? queue.Dequeue() : default(TResult));
    }
}

Usage example:

repository.Setup(x => x.GetPageByUrl<IPageModel>(virtualUrl)).ReturnsInOrder(null, pageModel.Object);

This method manages return value order through a queue, providing functionality similar to SetupSequence. While modern Moq versions include SetupSequence built-in, understanding this custom extension method offers insights into Moq's internal workings and serves as a reference for custom behaviors when needed.

Complete Test Case Implementation

Integrating the above solutions, a complete unit test example is provided below. Here, the SetupSequence method is used due to its directness and lack of external dependencies:

[TestCase("~/page/myaction")]
[TestCase("~/page/myaction/")]
public void Page_With_Custom_Action(string virtualUrl)
{
    // Arrange
    var pathData = new Mock<IPathData>();
    var pageModel = new Mock<IPageModel>();
    var repository = new Mock<IPageRepository>();
    var mapper = new Mock<IControllerMapper>();
    var container = new Mock<IContainer>();

    container.Setup(x => x.GetInstance<IPageRepository>()).Returns(repository.Object);
    repository.SetupSequence(x => x.GetPageByUrl<IPageModel>(virtualUrl))
        .Returns(null)
        .Returns(pageModel.Object);

    pathData.Setup(x => x.Action).Returns("myaction");
    pathData.Setup(x => x.Controller).Returns("page");

    var resolver = new DashboardPathResolver(pathData.Object, repository.Object, mapper.Object, container.Object);

    // Act
    var data = resolver.ResolvePath(virtualUrl);

    // Assert
    Assert.NotNull(data);
    Assert.AreEqual("myaction", data.Action);
    Assert.AreEqual("page", data.Controller);
}

In this test, GetPageByUrl<IPageModel> is configured to return null on the first call and pageModel.Object on the second call. This simulates real-world scenarios where an initial query might return no results, and a subsequent query succeeds, ensuring that DashboardPathResolver handles such cases correctly.

Technical Details and Considerations

When using SetupSequence, several points should be noted: First, the return value types must match the mocked method's return type; otherwise, a runtime exception will be thrown. Second, if the mocked method is called more times than the number of configured return values, subsequent calls will repeat the last return value. For example, with .Returns(null).Returns(pageModel.Object), the third and subsequent calls will return pageModel.Object. For more complex behaviors, such as cycling through values or throwing exceptions, the Throws method can be combined:

repository.SetupSequence(x => x.GetPageByUrl<IPageModel>(virtualUrl))
    .Throws(new InvalidOperationException("First call fails"))
    .Returns(pageModel.Object);

Additionally, SetupSequence supports generic methods and parameter matching. For instance, different return sequences can be configured for different parameter values:

repository.SetupSequence(x => x.GetPageByUrl<IPageModel>(It.IsAny<string>()))
    .Returns(null)
    .Returns(pageModel.Object);

However, note that parameter matchers like It.IsAny<string>() affect all calls, so they may not be suitable for scenarios requiring different sequences based on parameter values. In such cases, separate mocks might be needed for each parameter value.

Best Practices and Conclusion

When simulating different return values on multiple method calls in unit tests, prioritize using Moq's built-in SetupSequence method, as it is specifically designed for this scenario, offering concise and maintainable code. For older Moq versions or special requirements, custom extension methods like ReturnsInOrder provide viable alternatives. Regardless of the approach, ensure test scenarios are clear, return value orders are explicit, and edge cases (e.g., calls exceeding expectations) are considered. By precisely controlling mock behaviors, developers can write more reliable and comprehensive unit tests, thereby enhancing code quality and maintainability.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.