[C#] Testing Class Logic Using Partial Mocks with xUnit and Moq

目次

Overview

This article explains “Partial Mocking,” a technique using xUnit and Moq to mock only specific methods of a class while keeping the original implementation for the remaining methods. This is effective when there is a need to isolate unstable methods—such as random number generation or getting the current time—to focus on testing the main logic that uses those results.

Specifications (Input/Output)

  • Test Target: A class that generates random security codes.
  • Input: The length of the code to generate (integer).
  • Mock Setup: Fixing the random index generation to return a specific value or a specific sequence.
  • Output: Verifying that the generated string matches the pattern defined by the mock (e.g., “AAAA” or “ABCD”).
  • Technical Requirements:
    • Use Moq’s CallBase = true to execute original implementations for methods that are not set up.
    • Methods to be mocked must be marked as virtual.

Basic Usage

To keep the original implementation while overriding only a part of the class, set CallBase = true.

// Set CallBase = true when creating the mock
var mock = new Mock<MyClass> { CallBase = true };

// Override only virtual methods
mock.Setup(m => m.GetRandomValue()).Returns(100);

// Non-overridden methods run the original implementation
var result = mock.Object.Calculate(); 

Full Code

This example demonstrates the xUnit test class structure. The Main method is included here only to allow a pseudo-check of the behavior in a console environment.

using System;
using System.Text;
using Xunit;
using Moq;

namespace PartialMockExample
{
    class Program
    {
        static void Main()
        {
            try
            {
                var test = new SecurityCodeGeneratorTests();
                
                Console.WriteLine("Test 1: Running fixed value test...");
                test.Generate_ReturnsFixedString_WhenRandomIsFixed();
                Console.WriteLine("-> Pass");

                Console.WriteLine("Test 2: Running sequence test...");
                test.Generate_ReturnsSequentialString_WhenRandomIsSequential();
                Console.WriteLine("-> Pass");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"-> Fail: {ex.Message}");
            }
        }
    }

    // --- Test Class ---
    public class SecurityCodeGeneratorTests
    {
        private readonly Mock<SecurityCodeGenerator> _mock;

        public SecurityCodeGeneratorTests()
        {
            // IMPORTANT: Setting CallBase = true ensures non-setup methods 
            // execute the original class implementation.
            _mock = new Mock<SecurityCodeGenerator> { CallBase = true };
        }

        [Fact]
        public void Generate_ReturnsFixedString_WhenRandomIsFixed()
        {
            // Arrange
            // Fix GetRandomIndex to always return 0 (the first character in the set)
            _mock.Setup(x => x.GetRandomIndex(It.IsAny<int>()))
                 .Returns(0);

            var generator = _mock.Object;

            // Act
            // The Generate method itself runs its original implementation
            var result = generator.Generate(4);

            // Assert
            // "A" (index 0) should be selected 4 times from the character set
            Assert.Equal("AAAA", result);
        }

        [Fact]
        public void Generate_ReturnsSequentialString_WhenRandomIsSequential()
        {
            // Arrange
            // Return 0, 1, 2, 3 in order for each call
            _mock.SetupSequence(x => x.GetRandomIndex(It.IsAny<int>()))
                 .Returns(0)
                 .Returns(1)
                 .Returns(2)
                 .Returns(3);

            var generator = _mock.Object;

            // Act
            var result = generator.Generate(4);

            // Assert
            // Indices 0, 1, 2, 3 should be selected in order
            Assert.Equal("ABCD", result);
        }
    }

    // --- Class to be Tested ---
    public class SecurityCodeGenerator
    {
        private readonly string _charSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

        // Main logic to be tested without mocking
        public string Generate(int length)
        {
            var sb = new StringBuilder(length);
            for (int i = 0; i < length; i++)
            {
                // The key point is calling a virtual method internally
                int index = GetRandomIndex(_charSet.Length);
                sb.Append(_charSet[index]);
            }
            return sb.ToString();
        }

        // Random number generation part
        // IMPORTANT: Must be virtual for Moq to override it
        public virtual int GetRandomIndex(int max)
        {
            // Returns a random value in production
            return Random.Shared.Next(max);
        }
    }
}

Output Example

Test 1: Running fixed value test...
-> Pass
Test 2: Running sequence test...
-> Pass

Customization Points

  • Visibility of Target Methods: To allow Moq to override a method, it must be public virtual or protected virtual (using Protected().Setup(...)). If making a method virtual is not desirable, consider extracting an interface, such as IRandomProvider.
  • Non-Random Use Cases: Time-dependent logic (e.g., expiration date checks) can be tested by creating a virtual method that wraps DateTime.Now and configuring it to return a specific date only during tests.

Points of Caution

  • Forgetting CallBase = true: If this setting is omitted, any method that is not set up (such as the Generate method under test) will return default values (e.g., null), making it impossible to perform a valid test.
  • Impact on Design: Making methods virtual solely for testing might weaken encapsulation. Partial Mocking should be considered a solution for legacy code. For cleaner designs, it is better to separate the random generation logic into a different interface and use Dependency Injection (DI).
  • Constructor Arguments: If the target class has a constructor with arguments, those arguments must be passed to the Mock constructor, such as new Mock<TargetClass>(arg1, arg2).

Application

Mocking Protected Methods

If there is a need to mock internal logic that is not public (protected virtual), use the Moq.Protected namespace.

using Moq.Protected;

// Mocking 'protected virtual int GetInternalValue()'
mock.Protected()
    .Setup<int>("GetInternalValue")
    .Returns(99);

Summary

Implementing a Partial Mock with xUnit and Moq is possible by combining virtual methods with CallBase = true. This enables strict unit testing of logic even in classes containing uncertain elements like random numbers or time. Use this method appropriately, as overusing it can lead to a class design driven too heavily by testing requirements.

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次