Unit Testing Classes with Dependency Injection in C# using xUnit and Moq

目次

Overview

This article explains a unit testing method for classes that use the Dependency Injection (DI) pattern, which is a standard in modern C# development. Instead of directly creating instances of classes that access external services or databases, we inject mock objects through interfaces. This allows you to verify logic independently from external dependencies.

Specifications (Input/Output)

  • Target Class: ResponseProcessor. It contains logic to parse raw string data from an external service (e.g., “Result:SUCCESS”) and return a formatted value (e.g., “success”).
  • Dependency Interface: IDataFetcher. It handles data retrieval. We mock this during testing.
  • Input: Arguments for the test method (such as an ID) and a defined mock behavior that returns a fixed string for a specific ID.
  • Output: Verification that the method returns the correctly processed result based on the mock data.

Basic Usage

To test a class using “Constructor Injection,” pass the object created with Moq into the constructor when instantiating the class.

// 1. Create a mock for the dependency interface
var mock = new Mock<IDataFetcher>();

// 2. Define the behavior (Return "Status:OK" for ID: 100)
mock.Setup(x => x.GetData(100)).Returns("Status:OK");

// 3. Inject the mock into the constructor to create the target instance
var processor = new ResponseProcessor(mock.Object);

// 4. Verify the result
var result = processor.GetFormattedStatus(100);
Assert.Equal("ok", result);

Full Code

Below is the complete test code using xUnit and Moq.

using System;
using Xunit;
using Moq;

namespace DependencyInjectionTest
{
    class Program
    {
        static void Main()
        {
            // For manual execution check
            var testClass = new ResponseProcessorTests();
            try
            {
                testClass.GetFormattedStatus_ReturnsLowerCaseValue();
                Console.WriteLine("Test Passed: GetFormattedStatus_ReturnsLowerCaseValue");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Test Failed: {ex.Message}");
            }
        }
    }

    // --- Test Code ---
    public class ResponseProcessorTests
    {
        [Fact]
        public void GetFormattedStatus_ReturnsLowerCaseValue()
        {
            // Arrange
            int testId = 99;
            string rawResponse = "Result=Active";
            string expected = "active"; // Lowercase version of "Active"

            // Create the mock
            var mockFetcher = new Mock<IDataFetcher>();

            // Setup the mock behavior
            mockFetcher.Setup(m => m.FetchData(testId))
                       .Returns(rawResponse);

            // Inject the dependency
            var processor = new ResponseProcessor(mockFetcher.Object);

            // Act
            var actual = processor.GetFormattedStatus(testId);

            // Assert
            Assert.Equal(expected, actual);
        }
    }

    // --- Implementation Code ---

    public interface IDataFetcher
    {
        string FetchData(int id);
    }

    public class ResponseProcessor
    {
        private readonly IDataFetcher _fetcher;

        public ResponseProcessor(IDataFetcher fetcher)
        {
            _fetcher = fetcher ?? throw new ArgumentNullException(nameof(fetcher));
        }

        public string GetFormattedStatus(int id)
        {
            var rawData = _fetcher.FetchData(id);

            if (string.IsNullOrEmpty(rawData))
            {
                return string.Empty;
            }

            var parts = rawData.Split('=');
            if (parts.Length < 2)
            {
                return string.Empty;
            }

            return parts[1].ToLower();
        }
    }
}

Output Example

Test Passed: GetFormattedStatus_ReturnsLowerCaseValue

Customization Points

  • Injecting Multiple Interfaces: If your class depends on multiple services like loggers or settings, create separate mocks for each and pass them all to the constructor: new Target(mockFetch.Object, mockLog.Object).
  • Flexible Setups: Use Setup(m => m.FetchData(It.IsAny<int>())) to return a value regardless of what ID is passed.
  • Testing Error Handling: You can simulate external service errors by using mock.Setup(...).Throws(new Exception()). This verifies if your try-catch logic works correctly.

Points of Caution

  • Importance of Interface Design: Moq generally only works with interfaces or virtual methods. Follow the Dependency Inversion Principle (DIP) and extract interfaces for classes with external dependencies.
  • Missing Mock Setups: If a method is called with arguments that were not set up, Moq returns a default value (like null). This often causes NullReferenceException. Use It.IsAny<T>() or consider using MockBehavior.Strict.
  • Separate Logic from Instantiation: If you use new DataFetcher() inside your class, you cannot inject a mock. Always pass dependencies from the outside.

Application

Handling Property Injection

You can also test classes that receive dependencies through properties.

var processor = new ResponseProcessor();
// Inject the mock via property
processor.Fetcher = mock.Object;

var result = processor.GetFormattedStatus(100);

However, constructor injection is recommended because it prevents you from forgetting to initialize required dependencies.

Summary

Combining the DI pattern with Moq provides several benefits:

Fast Execution: Tests finish instantly because heavy processes are replaced by mocks. The “DI + Moq + xUnit” combination is the standard approach for unit testing in modern development.

Environment Independence: Run tests without needing a network or database.

Reproduce Error Cases: Easily simulate errors like connection timeouts that are hard to trigger in real environments.

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

この記事を書いた人

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

目次