| name | dotnet-testing-nsubstitute-mocking |
| description | Specialized skill for creating test doubles (Mock, Stub, Spy) using NSubstitute. Use when isolating external dependencies, simulating interface behavior, and verifying method calls. Covers complete guidance on Substitute.For, Returns, Received, Throws, etc. |
NSubstitute Mocking Skill
Skill Overview
This skill focuses on using NSubstitute to create and manage test doubles, covering all five Test Double types, dependency isolation strategies, behavior setup, and verification best practices.
Why Test Doubles?
Real-world code typically depends on external resources, making tests:
- Slow - Requires actual database, file system, or network operations
- Unstable - External service failures cause test failures
- Non-reproducible - Time, random numbers cause inconsistent results
- Environment-dependent - Requires specific external environment setup
- Development-blocking - Must wait for external systems to be ready
Test doubles enable 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.1.0" />
Basic using Statements
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
using AwesomeAssertions;
using Microsoft.Extensions.Logging;
Five Test Double Types
Based on Gerard Meszaros' definitions in xUnit Test Patterns:
1. Dummy - Filler Object
Used only to satisfy method signatures, never actually used.
public interface IEmailService
{
void SendEmail(string to, string subject, string body, ILogger logger);
}
[Fact]
public void ProcessOrder_LoggerNotUsed_ShouldProcessOrderSuccessfully()
{
var dummyLogger = Substitute.For<ILogger>();
var service = new OrderService();
var result = service.ProcessOrder(order, dummyLogger);
result.Success.Should().BeTrue();
}
2. Stub - Predefined Return Values
Provides predefined return values for testing specific scenarios.
[Fact]
public void GetUser_ValidUserId_ShouldReturnUserData()
{
var stubRepository = Substitute.For<IUserRepository>();
stubRepository.GetById(123).Returns(new User { Id = 123, Name = "John" });
var service = new UserService(stubRepository);
var actual = service.GetUser(123);
actual.Name.Should().Be("John");
}
3. Fake - Simplified Implementation
Has actual functionality but simplified, typically used for integration tests.
public class FakeUserRepository : IUserRepository
{
private readonly Dictionary<int, User> _users = new();
public User GetById(int id) => _users.TryGetValue(id, out var user) ? user : null;
public void Save(User user) => _users[user.Id] = user;
public void Delete(int id) => _users.Remove(id);
}
[Fact]
public void CreateUser_CreateUser_ShouldSaveAndRetrieve()
{
var fakeRepository = new FakeUserRepository();
var service = new UserService(fakeRepository);
service.CreateUser(new User { Id = 1, Name = "John" });
var actual = service.GetUser(1);
actual.Name.Should().Be("John");
}
4. Spy - Call Recording
Records how it's called, can verify calls afterward.
[Fact]
public void CreateUser_CreateUser_ShouldLogCreationInfo()
{
var spyLogger = Substitute.For<ILogger<UserService>>();
var repository = Substitute.For<IUserRepository>();
var service = new UserService(repository, spyLogger);
service.CreateUser(new User { Name = "John" });
spyLogger.Received(1).LogInformation("User created: {Name}", "John");
}
5. Mock - Behavior Verification
Expects specific interaction behavior, test fails if expectations aren't met.
[Fact]
public void RegisterUser_RegisterUser_ShouldSendWelcomeEmail()
{
var mockEmailService = Substitute.For<IEmailService>();
var repository = Substitute.For<IUserRepository>();
var service = new UserService(repository, mockEmailService);
service.RegisterUser("john@example.com", "John");
mockEmailService.Received(1).SendWelcomeEmail("john@example.com", "John");
}
NSubstitute Core Features
Basic Substitution Syntax
var substitute = Substitute.For<IUserRepository>();
var classSubstitute = Substitute.For<BaseService>();
var multiSubstitute = Substitute.For<IService, IDisposable>();
Return Value Setup
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");
Throw 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();
});
Real-World Patterns
Pattern 1: Dependency Injection & Test Setup
System Under Test
public class FileBackupService
{
private readonly IFileSystem _fileSystem;
private readonly IDateTimeProvider _dateTimeProvider;
private readonly IBackupRepository _backupRepository;
private readonly ILogger<FileBackupService> _logger;
public FileBackupService(
IFileSystem fileSystem,
IDateTimeProvider dateTimeProvider,
IBackupRepository backupRepository,
ILogger<FileBackupService> logger)
{
_fileSystem = fileSystem;
_dateTimeProvider = dateTimeProvider;
_backupRepository = backupRepository;
_logger = logger;
}
public async Task<BackupResult> BackupFileAsync(string sourcePath, string destinationPath)
{
if (!_fileSystem.FileExists(sourcePath))
{
_logger.LogWarning("Source file not found: {Path}", sourcePath);
return new BackupResult { Success = false, Message = "Source file not found" };
}
var fileInfo = _fileSystem.GetFileInfo(sourcePath);
if (fileInfo.Length > 100 * 1024 * 1024)
{
return new BackupResult { Success = false, Message = "File too large" };
}
var timestamp = _dateTimeProvider.Now.ToString("yyyyMMdd_HHmmss");
var backupFileName = $"{Path.GetFileNameWithoutExtension(sourcePath)}_{timestamp}{Path.GetExtension(sourcePath)}";
var fullBackupPath = Path.Combine(destinationPath, backupFileName);
_fileSystem.CopyFile(sourcePath, fullBackupPath);
await _backupRepository.SaveBackupHistory(sourcePath, fullBackupPath, _dateTimeProvider.Now);
_logger.LogInformation("Backup completed: {Path}", fullBackupPath);
return new BackupResult { Success = true, BackupPath = fullBackupPath };
}
}
Test Class Setup
public class FileBackupServiceTests
{
private readonly IFileSystem _fileSystem;
private readonly IDateTimeProvider _dateTimeProvider;
private readonly IBackupRepository _backupRepository;
private readonly ILogger<FileBackupService> _logger;
private readonly FileBackupService _sut;
public FileBackupServiceTests()
{
_fileSystem = Substitute.For<IFileSystem>();
_dateTimeProvider = Substitute.For<IDateTimeProvider>();
_backupRepository = Substitute.For<IBackupRepository>();
_logger = Substitute.For<ILogger<FileBackupService>>();
_sut = new FileBackupService(_fileSystem, _dateTimeProvider, _backupRepository, _logger);
}
[Fact]
public async Task BackupFileAsync_FileExistsAndSizeReasonable_ShouldBackupSuccessfully()
{
var sourcePath = @"C:\source\test.txt";
var destinationPath = @"C:\backup";
var testTime = new DateTime(2024, 1, 1, 12, 0, 0);
_fileSystem.FileExists(sourcePath).Returns(true);
_fileSystem.GetFileInfo(sourcePath).Returns(new FileInfo { Length = 1024 });
_dateTimeProvider.Now.Returns(testTime);
var result = await _sut.BackupFileAsync(sourcePath, destinationPath);
result.Success.Should().BeTrue();
result.BackupPath.Should().Be(@"C:\backup\test_20240101_120000.txt");
_fileSystem.Received(1).CopyFile(sourcePath, result.BackupPath);
await _backupRepository.Received(1).SaveBackupHistory(
sourcePath, result.BackupPath, testTime);
}
}
Pattern 2: Mock vs Stub Real-World Differences
Stub: Focus on State
[Fact]
public void CalculateDiscount_PremiumMember_ShouldReturn20Discount()
{
var stubCustomerService = Substitute.For<ICustomerService>();
stubCustomerService.GetCustomerType(123).Returns(CustomerType.Premium);
var service = new PricingService(stubCustomerService);
var discount = service.CalculateDiscount(123, 1000);
discount.Should().Be(200);
}
Mock: Focus on Behavior
[Fact]
public void ProcessPayment_SuccessfulPayment_ShouldLogTransactionInfo()
{
var mockLogger = Substitute.For<ILogger<PaymentService>>();
var stubPaymentGateway = Substitute.For<IPaymentGateway>();
stubPaymentGateway.ProcessPayment(Arg.Any<decimal>()).Returns(PaymentResult.Success);
var service = new PaymentService(stubPaymentGateway, mockLogger);
service.ProcessPayment(100);
mockLogger.Received(1).LogInformation(
"Payment processed: {Amount} - Result: {Result}",
100,
PaymentResult.Success);
}
Pattern 3: Async Method Testing
[Fact]
public async Task GetUserAsync_UserExists_ShouldReturnUserData()
{
var repository = Substitute.For<IUserRepository>();
repository.GetByIdAsync(123).Returns(Task.FromResult(
new User { Id = 123, Name = "John" }));
var service = new UserService(repository);
var result = await service.GetUserAsync(123);
result.Name.Should().Be("John");
await repository.Received(1).GetByIdAsync(123);
}
[Fact]
public async Task SaveUserAsync_DatabaseError_ShouldThrowException()
{
var repository = Substitute.For<IUserRepository>();
repository.SaveAsync(Arg.Any<User>())
.Throws(new InvalidOperationException("Database error"));
var service = new UserService(repository);
await service.SaveUserAsync(new User { Name = "John" })
.Should().ThrowAsync<InvalidOperationException>()
.WithMessage("Database error");
}
Pattern 4: ILogger Verification
Due to ILogger's extension method nature, verify the underlying Log method:
[Fact]
public async Task BackupFileAsync_FileNotExists_ShouldLogWarning()
{
var sourcePath = @"C:\nonexistent\test.txt";
_fileSystem.FileExists(sourcePath).Returns(false);
var result = await _sut.BackupFileAsync(sourcePath, @"C:\backup");
result.Success.Should().BeFalse();
_logger.Received(1).Log(
LogLevel.Warning,
Arg.Any<EventId>(),
Arg.Is<object>(v => v.ToString().Contains("Source file not found")),
null,
Arg.Any<Func<object, Exception, string>>());
}
Pattern 5: Complex Setup Management
Use base test class to manage shared setup:
public class OrderServiceTestsBase
{
protected readonly IOrderRepository Repository;
protected readonly IEmailService EmailService;
protected readonly ILogger<OrderService> Logger;
protected readonly OrderService Sut;
protected OrderServiceTestsBase()
{
Repository = Substitute.For<IOrderRepository>();
EmailService = Substitute.For<IEmailService>();
Logger = Substitute.For<ILogger<OrderService>>();
Sut = new OrderService(Repository, EmailService, Logger);
}
protected void SetupValidOrder(int orderId = 1)
{
Repository.GetById(orderId).Returns(
new Order { Id = orderId, Status = OrderStatus.Pending });
}
protected void SetupEmailServiceSuccess()
{
EmailService.SendConfirmation(Arg.Any<string>()).Returns(true);
}
}
public class OrderServiceTests : OrderServiceTestsBase
{
[Fact]
public void ProcessOrder_ValidOrder_ShouldProcessSuccessfully()
{
SetupValidOrder();
SetupEmailServiceSuccess();
var result = Sut.ProcessOrder(1);
result.Success.Should().BeTrue();
}
}
Advanced Argument Matching Techniques
Complex Object Matching
[Fact]
public void CreateOrder_CreateOrder_ShouldSaveCorrectOrderData()
{
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 & Verification
[Fact]
public void RegisterUser_RegisterUser_ShouldGenerateCorrectPasswordHash()
{
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 & Best Practices
✅ Recommended Practices
-
Create Substitute for Interfaces, Not Implementations
var repository = Substitute.For<IUserRepository>();
var repository = Substitute.For<UserRepository>();
-
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" };
-
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>());
-
Clear Distinction Between Mock and Stub
var stubRepository = Substitute.For<IUserRepository>();
var mockLogger = Substitute.For<ILogger>();
stubRepository.GetById(123).Returns(user);
service.ProcessUser(123);
mockLogger.Received(1).LogInformation(Arg.Any<string>());
❌ Practices to Avoid
-
Avoid Mocking Value Types
var badDate = Substitute.For<DateTime>();
var dateTimeProvider = Substitute.For<IDateTimeProvider>();
dateTimeProvider.Now.Returns(new DateTime(2024, 1, 1));
-
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);
-
Avoid Overly Complex Setup
var sub1 = Substitute.For<IService1>();
var sub2 = Substitute.For<IService2>();
var sub3 = Substitute.For<IService3>();
var sub4 = Substitute.For<IService4>();
Identifying Dependencies to Substitute
Should Substitute
- ✅ 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 calculations (IComplexCalculator)
- ✅ Logging services (ILogger)
Should Not Substitute
- ❌ Value objects (DateTime, string, int)
- ❌ Simple data transfer objects (DTO)
- ❌ Pure function utilities (like AutoMapper's IMapper, consider real instance)
- ❌ Framework core classes (unless specific need)
Troubleshooting
Q1: How to test classes without interfaces?
A: Ensure members to mock are virtual:
public class BaseService
{
public virtual string GetData() => "real data";
}
var substitute = Substitute.For<BaseService>();
substitute.GetData().Returns("test data");
Q2: How to verify method call order?
A: Use Received.InOrder():
Received.InOrder(() =>
{
_service.Start();
_service.Process();
_service.Stop();
});
Q3: How to handle out parameters?
A: Use Returns() with delegate:
_service.TryGetValue("key", out Arg.Any<string>())
.Returns(x =>
{
x[1] = "value";
return true;
});
Q4: NSubstitute vs Moq - Which to choose?
A: NSubstitute advantages:
- More concise, intuitive syntax
- Gentler learning curve
- No privacy concerns
- Sufficient for most testing scenarios
Choose NSubstitute unless:
- Project already uses Moq
- Need Moq-specific advanced features
- Team already familiar with Moq syntax
Integration with Other Skills
This skill can be combined with:
- unit-test-fundamentals: Unit testing basics and 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 Files Reference
This skill provides these template files:
templates/mock-patterns.cs: Complete Mock/Stub/Spy pattern examples
templates/verification-examples.cs: Behavior verification and argument matching examples
Reference Resources
Original Articles
Content distilled from "Old-School Software Engineer's Testing Practice - 30 Day Challenge" series:
- Day 07 - Dependency Substitution Introduction: Using NSubstitute
NSubstitute Official
Test Double Theory