SuperAPI AI Directory

dotnet-testing-nsubstitute-mocking

Testing & quality

View repository

Updated 10-15 23:45

About this skill

NSubstitute Test Doubles Guide

Why Do We Need Test Doubles?

Real-world code usually depends on external resources, and these dependencies make tests:

  1. Slow - Requires actually operating databases, file systems, networks
  2. Unstable - External service anomalies cause test failures
  3. Hard to reproduce - Time and random numbers cause inconsistent results
  4. Environment-dependent - Requires specific external environment configuration
  5. Development blocking - Must wait for external systems to be ready

Test Doubles allow us to isolate these dependencies and focus on testing business logic.

Prerequisites

Package Installation

<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="AwesomeAssertions" Version="9.4.0" />

Basic using Directives

using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
using AwesomeAssertions;
using Microsoft.Extensions.Logging;

The Five Major Types of Test Doubles

According to Gerard Meszaros's definition in "xUnit Test Patterns," test doubles are divided into five types:

Type Purpose NSubstitute Equivalent
Dummy Filler object that only satisfies method signatures Substitute.For<T>() Do not configure any behavior
Stub Provides default return values, sets up test scenarios .Returns(value)
Fake Simplified implementation with real logic Manually implement the interface (e.g., FakeUserRepository)
Spy Records calls, verifies afterward .Received() Verify
Mock Expects interactions by default, test fails if not satisfied .Received(n) Strict verification

For complete code examples of each type, see references/test-double-types.md

NSubstitute Core Features

Basic Substitute Syntax

// 建立介面替代
var substitute = Substitute.For<IUserRepository>();

// 建立類別替代(需要虛擬成員)
var classSubstitute = Substitute.For<BaseService>();

// 建立多重介面替代
var multiSubstitute = Substitute.For<IService, IDisposable>();

Return Value Configuration

Basic Return Values

// 精確參數匹配
_repository.GetById(1).Returns(new User { Id = 1, Name = "John" });

// 任意參數匹配
_service.Process(Arg.Any<string>()).Returns("processed");

// 回傳序列值
_generator.GetNext().Returns(1, 2, 3, 4, 5);

Conditional Return Values

// 使用委派計算回傳值
_calculator.Add(Arg.Any<int>(), Arg.Any<int>())
           .Returns(x => (int)x[0] + (int)x[1]);

// 條件匹配
_service.Process(Arg.Is<string>(x => x.StartsWith("test")))
        .Returns("test-result");

Throwing Exceptions

// 同步方法拋出例外
_service.RiskyOperation()
        .Throws(new InvalidOperationException("Something went wrong"));

// 非同步方法拋出例外
_service.RiskyOperationAsync()
        .Throws(new InvalidOperationException("Async operation failed"));

Argument Matchers

// 任意值
_service.Process(Arg.Any<string>()).Returns("result");

// 特定條件
_service.Process(Arg.Is<string>(x => x.Length > 5)).Returns("long-result");

// 引數擷取
string capturedArg = null;
_service.Process(Arg.Do<string>(x => capturedArg = x)).Returns("result");
_service.Process("test");
capturedArg.Should().Be("test");

// 引數檢查
_service.Process(Arg.Is<string>(x =>
{
    x.Should().StartWith("prefix");
    return true;
})).Returns("result");

Call Verification

// 驗證被呼叫(至少一次)
_service.Received().Process("test");

// 驗證呼叫次數
_service.Received(2).Process(Arg.Any<string>());

// 驗證未被呼叫
_service.DidNotReceive().Delete(Arg.Any<int>());

// 驗證任意參數呼叫
_service.ReceivedWithAnyArgs().Process(default);

// 驗證呼叫順序
Received.InOrder(() =>
{
    _service.Start();
    _service.Process();
    _service.Stop();
});

Practical Patterns

Covers five common NSubstitute practical patterns, including complete code examples:

Pattern Description
Pattern 1: Dependency Injection and Test Setup Complete FileBackupService example, including constructor injection and SUT setup
Pattern 2: Mock vs Stub Differences Stub focuses on state return values vs Mock focuses on interaction behavior verification
Pattern 3: Asynchronous Method Testing Returns(Task.FromResult(...)) and .Throws() patterns
Pattern 4: ILogger Verification Verify the underlying Log method to bypass extension method limitations
Pattern 5: Complex Configuration Management Base test class manages shared Substitute configuration

For complete code examples, see references/practical-patterns.md

Advanced Argument Matching Techniques

Complex Object Matching

[Fact]
public void CreateOrder_建立訂單_應儲存正確的訂單資料()
{
    var repository = Substitute.For<IOrderRepository>();
    var service = new OrderService(repository);

    service.CreateOrder("Product A", 5, 100);

    // 驗證物件屬性
    repository.Received(1).Save(Arg.Is<Order>(o =>
        o.ProductName == "Product A" &&
        o.Quantity == 5 &&
        o.Price == 100));
}

Argument Capture and Verification

[Fact]
public void RegisterUser_註冊使用者_應產生正確的雜湊密碼()
{
    var repository = Substitute.For<IUserRepository>();
    var service = new UserService(repository);

    User capturedUser = null;
    repository.Save(Arg.Do<User>(u => capturedUser = u));

    service.RegisterUser("john@example.com", "password123");

    capturedUser.Should().NotBeNull();
    capturedUser.Email.Should().Be("john@example.com");
    capturedUser.PasswordHash.Should().NotBe("password123"); // 應該被雜湊
    capturedUser.PasswordHash.Length.Should().BeGreaterThan(20);
}

Common Pitfalls and Best Practices

Recommended Practices

  1. Create Substitutes for interfaces rather than implementations

    // 正確:針對介面
    var repository = Substitute.For<IUserRepository>();
    
    // 錯誤:針對具體類別(除非有虛擬成員)
    var repository = Substitute.For<UserRepository>();
    
  2. Use meaningful test data

    // 正確:清楚表達意圖
    var user = new User { Id = 123, Name = "John Doe", Email = "john@example.com" };
    
    // 錯誤:無意義的資料
    var user = new User { Id = 1, Name = "test", Email = "a@b.c" };
    
  3. Avoid over-verification

    // 正確:只驗證重要的行為
    _emailService.Received(1).SendWelcomeEmail(Arg.Any<string>());
    
    // 錯誤:驗證所有內部實作細節
    _repository.Received(1).GetById(123);
    _repository.Received(1).Update(Arg.Any<User>());
    _validator.Received(1).Validate(Arg.Any<User>());
    
  4. Clearly distinguish between Mock and Stub

    // 正確:Stub 用於設定情境,Mock 用於驗證行為
    var stubRepository = Substitute.For<IUserRepository>(); // Stub
    var mockLogger = Substitute.For<ILogger>(); // Mock
    
    stubRepository.GetById(123).Returns(user);
    service.ProcessUser(123);
    mockLogger.Received(1).LogInformation(Arg.Any<string>());
    

Practices to Avoid

  1. Avoid mocking value types

    // 錯誤:DateTime 是值類型
    var badDate = Substitute.For<DateTime>();
    
    // 正確:抽象時間提供者
    var dateTimeProvider = Substitute.For<IDateTimeProvider>();
    dateTimeProvider.Now.Returns(new DateTime(2024, 1, 1));
    
  2. Avoid tight coupling between tests and implementation

    // 錯誤:測試實作細節
    _repository.Received(1).Query(Arg.Any<string>());
    _repository.Received(1).Filter(Arg.Any<Expression<Func<User, bool>>>());
    
    // 正確:測試行為結果
    var users = service.GetActiveUsers();
    users.Should().HaveCount(2);
    
  3. Avoid overly complex setup

    // 錯誤:過多的 Substitute(可能違反 SRP)
    var sub1 = Substitute.For<IService1>();
    var sub2 = Substitute.For<IService2>();
    var sub3 = Substitute.For<IService3>();
    var sub4 = Substitute.For<IService4>();
    
    // 正確:重新思考類別職責
    // 考慮是否違反單一職責原則,需要重構
    

Identify Dependencies That Need to Be Substituted

Should Be Substituted

  • External API calls (IHttpClient, IApiClient)
  • Database operations (IRepository, IDbContext)
  • File system operations (IFileSystem)
  • Network communication (IEmailService, IMessageQueue)
  • Time dependencies (IDateTimeProvider, TimeProvider)
  • Random number generation (IRandom)
  • Expensive computations (IComplexCalculator)
  • Logging services (ILogger)

Should Not Be Substituted

  • Value objects (DateTime, string, int)
  • Simple data transfer objects (DTO)
  • Pure function utilities (such as AutoMapper's IMapper; consider using a real instance)
  • Framework core classes (unless there is a clear need)

Troubleshooting

Q1: How do I test a class that has no interface?

A: Ensure the member to be mocked is virtual:

public class BaseService
{
    public virtual string GetData() => "real data";
}

var substitute = Substitute.For<BaseService>();
substitute.GetData().Returns("test data");

Q2: How do I verify the order in which methods are called?

A: Use Received.InOrder():

Received.InOrder(() =>
{
    _service.Start();
    _service.Process();
    _service.Stop();
});

Q3: How do I handle out parameters?

A: Use Returns() with a delegate:

_service.TryGetValue("key", out Arg.Any<string>())
        .Returns(x =>
        {
            x[1] = "value";
            return true;
        });

Q4: How should I choose between NSubstitute and Moq?

A: NSubstitute advantages:

  • More concise and intuitive syntax
  • Gentle learning curve
  • No privacy controversy
  • Sufficient for most testing scenarios

Choose NSubstitute unless:

  • The project already uses Moq
  • You need Moq-specific advanced features
  • The team is already familiar with Moq syntax

Integration with Other Skills

This skill can be used in combination with the following skills:

  • unit-test-fundamentals: Unit testing fundamentals and the 3A pattern
  • dependency-injection-testing: Dependency injection testing strategies
  • test-naming-conventions: Test naming conventions
  • test-output-logging: ITestOutputHelper and ILogger integration
  • datetime-testing-timeprovider: TimeProvider for abstracting time dependencies
  • filesystem-testing-abstractions: File system dependency abstraction

Template File Reference

This skill provides the following template files:

  • templates/mock-patterns.cs: Complete Mock/Stub/Spy pattern examples
  • templates/verification-examples.cs: Behavior verification and argument matching examples
  • references/practical-patterns.md: Complete code for five practical patterns
  • references/test-double-types.md: Detailed examples of the five major types of Test Double

Output Format

  • Generate an xUnit test class file using NSubstitute (*Tests.cs)
  • Test methods follow the 3A (Arrange-Act-Assert) pattern
  • Substitute setup is centralized in the test constructor or the Arrange section
  • Verification calls use Received() / DidNotReceive() explicit assertions
  • Use AwesomeAssertions for state verification

Reference Resources

Original Articles

The content of this skill is distilled from the article series "Old-School Software Engineer's Testing Training - 30-Day Challenge":

NSubstitute Official

Test Double Theory