Mocking and Test Doubles
Overview
Test doubles are objects that stand in for real dependencies during testing. Understanding when and how to use different types of test doubles is essential for writing effective, maintainable unit tests.
Table of Contents
- Test Double Types
- Gerard Meszarosâ Classification
- Moq Deep Dive
- NSubstitute Deep Dive
- FakeItEasy Overview
- When to Mock
- When NOT to Mock
- Auto-Mocking Containers
- Mocking Common .NET Types
- Anti-Patterns
- Interview Questions
Test Double Types
| Type | Purpose | Example Use Case |
|---|---|---|
| Dummy | Fill parameter lists, never used | Required constructor parameter that isnât accessed |
| Stub | Provide canned answers to calls | Repository returning predefined data |
| Spy | Record information about calls | Logging service tracking method calls |
| Mock | Pre-programmed with expectations | Verify email service was called correctly |
| Fake | Working implementation with shortcuts | In-memory database instead of SQL Server |
Gerard Meszarosâ Classification
Dummy
Passed around but never actually used. Typically fills parameter lists.
public class DummyExample
{
[Fact]
public void Constructor_WithLogger_DoesNotThrow()
{
// Logger is required but not used in this test
ILogger<OrderService> dummyLogger = null!;
var repository = Substitute.For<IOrderRepository>();
// We only care that construction succeeds
var service = new OrderService(repository, dummyLogger);
Assert.NotNull(service);
}
}
Stub
Provides canned answers to calls made during the test. Doesnât respond to anything outside whatâs programmed.
public class StubExample
{
[Fact]
public async Task GetOrder_ReturnsOrderFromRepository()
{
// Arrange - Stub returns canned data
var stubRepository = Substitute.For<IOrderRepository>();
var expectedOrder = new Order { Id = Guid.NewGuid(), Total = 100m };
stubRepository.GetByIdAsync(Arg.Any<Guid>())
.Returns(expectedOrder);
var service = new OrderService(stubRepository);
// Act
var result = await service.GetOrderAsync(expectedOrder.Id);
// Assert - We verify the result, not how the stub was called
Assert.Equal(expectedOrder.Total, result.Total);
}
}
Spy
A stub that also records information about how it was called.
public class SpyExample
{
private class EmailServiceSpy : IEmailService
{
public List<(string To, string Subject, string Body)> SentEmails { get; } = new();
public Task SendAsync(string to, string subject, string body)
{
SentEmails.Add((to, subject, body));
return Task.CompletedTask;
}
}
[Fact]
public async Task ProcessOrder_SendsConfirmationEmail()
{
// Arrange
var emailSpy = new EmailServiceSpy();
var service = new OrderService(emailSpy);
var order = new Order { CustomerEmail = "test@example.com" };
// Act
await service.ProcessAsync(order);
// Assert - Check what the spy recorded
Assert.Single(emailSpy.SentEmails);
Assert.Equal("test@example.com", emailSpy.SentEmails[0].To);
Assert.Contains("confirmation", emailSpy.SentEmails[0].Subject.ToLower());
}
}
Mock
Objects pre-programmed with expectations about the calls they will receive.
public class MockExample
{
[Fact]
public async Task ProcessOrder_CallsRepositorySaveExactlyOnce()
{
// Arrange
var mockRepository = new Mock<IOrderRepository>();
var service = new OrderService(mockRepository.Object);
var order = new Order { Id = Guid.NewGuid() };
// Act
await service.ProcessAsync(order);
// Assert - Verify the expected interaction occurred
mockRepository.Verify(
r => r.SaveAsync(It.Is<Order>(o => o.Id == order.Id)),
Times.Once,
"Repository Save should be called exactly once with the order");
}
}
Fake
Working implementations with shortcuts (simplified behavior).
public class FakeExample
{
public class FakeOrderRepository : IOrderRepository
{
private readonly Dictionary<Guid, Order> _orders = new();
public Task<Order?> GetByIdAsync(Guid id)
{
_orders.TryGetValue(id, out var order);
return Task.FromResult(order);
}
public Task SaveAsync(Order order)
{
_orders[order.Id] = order;
return Task.CompletedTask;
}
public Task<IEnumerable<Order>> GetAllAsync()
{
return Task.FromResult<IEnumerable<Order>>(_orders.Values);
}
public Task DeleteAsync(Guid id)
{
_orders.Remove(id);
return Task.CompletedTask;
}
}
[Fact]
public async Task OrderWorkflow_CompleteCycle_WorksCorrectly()
{
// Arrange - Use fake with real behavior
var fakeRepository = new FakeOrderRepository();
var service = new OrderService(fakeRepository);
var order = new Order { Id = Guid.NewGuid(), Total = 100m };
// Act
await service.SaveAsync(order);
var retrieved = await service.GetOrderAsync(order.Id);
// Assert
Assert.Equal(order.Id, retrieved?.Id);
Assert.Equal(100m, retrieved?.Total);
}
}
Moq Deep Dive
Installation
dotnet add package Moq
Basic Setup and Returns
public class MoqBasics
{
[Fact]
public void Setup_ReturnsValue()
{
// Create mock
var mock = new Mock<IOrderRepository>();
// Setup return value
mock.Setup(r => r.GetByIdAsync(It.IsAny<Guid>()))
.ReturnsAsync(new Order { Total = 100m });
// Use the mock
var service = new OrderService(mock.Object);
}
}
Argument Matching
public class MoqArgumentMatching
{
[Fact]
public void ArgumentMatching_Examples()
{
var mock = new Mock<IOrderRepository>();
// Any value
mock.Setup(r => r.GetByIdAsync(It.IsAny<Guid>()))
.ReturnsAsync(new Order());
// Specific value
var specificId = Guid.NewGuid();
mock.Setup(r => r.GetByIdAsync(specificId))
.ReturnsAsync(new Order { Id = specificId });
// Conditional matching
mock.Setup(r => r.FindAsync(It.Is<string>(s => s.StartsWith("ORD-"))))
.ReturnsAsync(new List<Order>());
// Regex matching
mock.Setup(r => r.FindAsync(It.IsRegex(@"^[A-Z]{3}-\d{4}$")))
.ReturnsAsync(new List<Order>());
// Range matching
mock.Setup(r => r.GetOrdersInRange(
It.IsInRange(1, 100, Moq.Range.Inclusive),
It.IsInRange(1, 100, Moq.Range.Inclusive)))
.ReturnsAsync(new List<Order>());
}
}
Callbacks and Capturing Arguments
public class MoqCallbacks
{
[Fact]
public async Task Callback_CapturesArguments()
{
var mock = new Mock<IOrderRepository>();
Order? capturedOrder = null;
mock.Setup(r => r.SaveAsync(It.IsAny<Order>()))
.Callback<Order>(o => capturedOrder = o)
.Returns(Task.CompletedTask);
var service = new OrderService(mock.Object);
await service.ProcessAsync(new Order { Total = 150m });
Assert.NotNull(capturedOrder);
Assert.Equal(150m, capturedOrder.Total);
}
[Fact]
public void Callback_TrackMultipleCalls()
{
var mock = new Mock<IEmailService>();
var sentEmails = new List<string>();
mock.Setup(e => e.SendAsync(It.IsAny<string>(), It.IsAny<string>()))
.Callback<string, string>((to, subject) => sentEmails.Add(to))
.Returns(Task.CompletedTask);
// ... perform actions ...
Assert.Equal(3, sentEmails.Count);
}
}
Sequential Returns
public class MoqSequence
{
[Fact]
public async Task SetupSequence_ReturnsDifferentValues()
{
var mock = new Mock<IOrderRepository>();
mock.SetupSequence(r => r.GetNextOrderNumberAsync())
.ReturnsAsync("ORD-001")
.ReturnsAsync("ORD-002")
.ReturnsAsync("ORD-003")
.ThrowsAsync(new InvalidOperationException("No more numbers"));
var result1 = await mock.Object.GetNextOrderNumberAsync();
var result2 = await mock.Object.GetNextOrderNumberAsync();
var result3 = await mock.Object.GetNextOrderNumberAsync();
Assert.Equal("ORD-001", result1);
Assert.Equal("ORD-002", result2);
Assert.Equal("ORD-003", result3);
await Assert.ThrowsAsync<InvalidOperationException>(
() => mock.Object.GetNextOrderNumberAsync());
}
}
Verification
public class MoqVerification
{
[Fact]
public async Task Verify_MethodCalls()
{
var mock = new Mock<IOrderRepository>();
mock.Setup(r => r.SaveAsync(It.IsAny<Order>())).Returns(Task.CompletedTask);
var service = new OrderService(mock.Object);
await service.ProcessAsync(new Order());
// Verify exact call count
mock.Verify(r => r.SaveAsync(It.IsAny<Order>()), Times.Once);
mock.Verify(r => r.SaveAsync(It.IsAny<Order>()), Times.Exactly(1));
// Verify never called
mock.Verify(r => r.DeleteAsync(It.IsAny<Guid>()), Times.Never);
// Verify at least/at most
mock.Verify(r => r.SaveAsync(It.IsAny<Order>()), Times.AtLeastOnce);
mock.Verify(r => r.SaveAsync(It.IsAny<Order>()), Times.AtMost(2));
// Verify with specific arguments
mock.Verify(r => r.SaveAsync(It.Is<Order>(o => o.Status == OrderStatus.Pending)));
// Verify no other calls were made
mock.VerifyNoOtherCalls();
}
}
Strict vs Loose Mocking
public class MoqBehavior
{
[Fact]
public void StrictMock_ThrowsForUnexpectedCalls()
{
// Strict mock - throws if unexpected method is called
var strictMock = new Mock<IOrderRepository>(MockBehavior.Strict);
strictMock.Setup(r => r.GetByIdAsync(It.IsAny<Guid>()))
.ReturnsAsync(new Order());
// This will throw because SaveAsync wasn't set up
// strictMock.Object.SaveAsync(new Order());
}
[Fact]
public void LooseMock_ReturnsDefaultsForUnsetup()
{
// Loose mock (default) - returns default values for unsetup methods
var looseMock = new Mock<IOrderRepository>(MockBehavior.Loose);
// Returns null for unsetup async method
var result = looseMock.Object.GetByIdAsync(Guid.NewGuid()).Result;
Assert.Null(result);
}
}
Mocking Properties
public class MoqProperties
{
[Fact]
public void MockProperties()
{
var mock = new Mock<IConfiguration>();
// Setup property getter
mock.SetupGet(c => c.ConnectionString).Returns("Server=test;");
// Setup property to track changes
mock.SetupProperty(c => c.Timeout, TimeSpan.FromSeconds(30));
// Change the value
mock.Object.Timeout = TimeSpan.FromMinutes(1);
Assert.Equal(TimeSpan.FromMinutes(1), mock.Object.Timeout);
// Setup all properties to track changes
mock.SetupAllProperties();
}
}
Protected and Virtual Members
public abstract class BaseService
{
protected abstract Task<bool> ValidateInternalAsync(string input);
public async Task<bool> ProcessAsync(string input)
{
return await ValidateInternalAsync(input);
}
}
public class MoqProtected
{
[Fact]
public async Task MockProtectedMember()
{
var mock = new Mock<BaseService>();
mock.Protected()
.Setup<Task<bool>>("ValidateInternalAsync", ItExpr.IsAny<string>())
.ReturnsAsync(true);
var result = await mock.Object.ProcessAsync("test");
Assert.True(result);
}
}
NSubstitute Deep Dive
Installation
dotnet add package NSubstitute
Basic Setup
public class NSubstituteBasics
{
[Fact]
public async Task BasicSetup()
{
// Create substitute
var repository = Substitute.For<IOrderRepository>();
// Setup return value (more readable than Moq)
repository.GetByIdAsync(Arg.Any<Guid>())
.Returns(new Order { Total = 100m });
// Use in service
var service = new OrderService(repository);
var result = await service.GetOrderAsync(Guid.NewGuid());
Assert.Equal(100m, result.Total);
}
}
Argument Matching
public class NSubstituteArguments
{
[Fact]
public void ArgumentMatching()
{
var repository = Substitute.For<IOrderRepository>();
// Any value
repository.GetByIdAsync(Arg.Any<Guid>()).Returns(new Order());
// Specific value
var specificId = Guid.NewGuid();
repository.GetByIdAsync(specificId).Returns(new Order { Id = specificId });
// Conditional
repository.FindAsync(Arg.Is<string>(s => s.StartsWith("ORD-")))
.Returns(new List<Order>());
}
}
Returns Based on Arguments
public class NSubstituteReturns
{
[Fact]
public async Task ReturnBasedOnArgument()
{
var repository = Substitute.For<IOrderRepository>();
// Return based on input
repository.GetByIdAsync(Arg.Any<Guid>())
.Returns(callInfo => new Order
{
Id = callInfo.Arg<Guid>()
});
var id = Guid.NewGuid();
var result = await repository.GetByIdAsync(id);
Assert.Equal(id, result.Id);
}
[Fact]
public async Task MultipleReturns()
{
var repository = Substitute.For<IOrderRepository>();
// Different returns for each call
repository.GetNextOrderNumberAsync()
.Returns("ORD-001", "ORD-002", "ORD-003");
Assert.Equal("ORD-001", await repository.GetNextOrderNumberAsync());
Assert.Equal("ORD-002", await repository.GetNextOrderNumberAsync());
Assert.Equal("ORD-003", await repository.GetNextOrderNumberAsync());
}
}
Callbacks
public class NSubstituteCallbacks
{
[Fact]
public async Task CaptureArguments()
{
var repository = Substitute.For<IOrderRepository>();
Order? capturedOrder = null;
repository.SaveAsync(Arg.Do<Order>(o => capturedOrder = o));
await repository.SaveAsync(new Order { Total = 200m });
Assert.Equal(200m, capturedOrder?.Total);
}
[Fact]
public void AndDoesCallback()
{
var repository = Substitute.For<IOrderRepository>();
var callCount = 0;
repository.GetByIdAsync(Arg.Any<Guid>())
.Returns(new Order())
.AndDoes(_ => callCount++);
repository.GetByIdAsync(Guid.NewGuid());
repository.GetByIdAsync(Guid.NewGuid());
Assert.Equal(2, callCount);
}
}
Verification
public class NSubstituteVerification
{
[Fact]
public async Task VerifyCalls()
{
var repository = Substitute.For<IOrderRepository>();
var service = new OrderService(repository);
await service.ProcessAsync(new Order());
// Received exactly once
await repository.Received().SaveAsync(Arg.Any<Order>());
await repository.Received(1).SaveAsync(Arg.Any<Order>());
// Never received
await repository.DidNotReceive().DeleteAsync(Arg.Any<Guid>());
// Received with specific arguments
await repository.Received().SaveAsync(Arg.Is<Order>(o => o.Status == OrderStatus.Pending));
}
[Fact]
public async Task VerifyCallOrder()
{
var repository = Substitute.For<IOrderRepository>();
var emailService = Substitute.For<IEmailService>();
// Perform actions...
// Verify order of calls
Received.InOrder(async () =>
{
await repository.SaveAsync(Arg.Any<Order>());
await emailService.SendAsync(Arg.Any<string>(), Arg.Any<string>());
});
}
}
Throwing Exceptions
public class NSubstituteExceptions
{
[Fact]
public async Task ThrowException()
{
var repository = Substitute.For<IOrderRepository>();
// Throw synchronously
repository.GetByIdAsync(Arg.Any<Guid>())
.Throws(new NotFoundException("Order not found"));
// Throw asynchronously
repository.GetByIdAsync(Arg.Any<Guid>())
.ThrowsAsync(new NotFoundException("Order not found"));
await Assert.ThrowsAsync<NotFoundException>(
() => repository.GetByIdAsync(Guid.NewGuid()));
}
}
FakeItEasy Overview
Installation
dotnet add package FakeItEasy
Basic Usage
public class FakeItEasyBasics
{
[Fact]
public async Task BasicUsage()
{
// Create fake
var repository = A.Fake<IOrderRepository>();
// Setup
A.CallTo(() => repository.GetByIdAsync(A<Guid>._))
.Returns(new Order { Total = 100m });
// Verify
A.CallTo(() => repository.SaveAsync(A<Order>._))
.MustHaveHappened();
A.CallTo(() => repository.DeleteAsync(A<Guid>._))
.MustNotHaveHappened();
}
}
When to Mock
Good Candidates for Mocking
// 1. External Services (APIs, databases)
public class OrderServiceTests
{
[Fact]
public async Task ProcessPayment_MocksPaymentGateway()
{
var paymentGateway = Substitute.For<IPaymentGateway>();
paymentGateway.ProcessAsync(Arg.Any<PaymentRequest>())
.Returns(new PaymentResult { Success = true });
// Test without hitting real payment API
}
}
// 2. Non-deterministic Dependencies (time, random)
public class ExpirationServiceTests
{
[Fact]
public void IsExpired_MocksCurrentTime()
{
var timeProvider = Substitute.For<ITimeProvider>();
timeProvider.Now.Returns(new DateTime(2024, 6, 15));
var service = new ExpirationService(timeProvider);
var result = service.IsExpired(new DateTime(2024, 6, 14));
Assert.True(result);
}
}
// 3. Slow Dependencies (file system, network)
public class ReportServiceTests
{
[Fact]
public async Task GenerateReport_MocksFileSystem()
{
var fileSystem = Substitute.For<IFileSystem>();
fileSystem.ReadAllTextAsync(Arg.Any<string>())
.Returns("template content");
// Fast test without disk I/O
}
}
// 4. Dependencies with Side Effects (email, SMS)
public class NotificationServiceTests
{
[Fact]
public async Task SendNotification_MocksEmailService()
{
var emailService = Substitute.For<IEmailService>();
// Test logic without sending real emails
}
}
When NOT to Mock
Donât Mock These
// 1. Don't mock the class under test
// BAD:
var sut = Substitute.For<OrderService>(); // Never do this!
// GOOD:
var sut = new OrderService(mockDependencies);
// 2. Don't mock value objects / DTOs
// BAD:
var order = Substitute.For<Order>(); // Just create a real one!
// GOOD:
var order = new Order { Total = 100m };
// 3. Don't mock simple libraries
// BAD:
var json = Substitute.For<IJsonSerializer>();
// GOOD:
var json = JsonSerializer.Serialize(data); // Use real serialization
// 4. Don't mock Entity Framework DbContext directly
// BAD:
var context = Substitute.For<ApplicationDbContext>();
// GOOD:
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase("TestDb")
.Options;
var context = new ApplicationDbContext(options);
// 5. Don't mock types you don't own (carefully)
// Consider creating a wrapper interface instead
public interface IHttpClientWrapper
{
Task<HttpResponseMessage> SendAsync(HttpRequestMessage request);
}
Auto-Mocking Containers
AutoFixture with AutoMoq
// Install: dotnet add package AutoFixture.AutoMoq
public class AutoMoqTests
{
private readonly IFixture _fixture;
public AutoMoqTests()
{
_fixture = new Fixture().Customize(new AutoMoqCustomization());
}
[Fact]
public void AutoCreateServiceWithMocks()
{
// All dependencies automatically mocked
var service = _fixture.Create<OrderService>();
// Get the auto-created mock
var mockRepository = _fixture.Freeze<Mock<IOrderRepository>>();
mockRepository.Setup(r => r.GetByIdAsync(It.IsAny<Guid>()))
.ReturnsAsync(new Order());
// Service now uses the configured mock
}
}
Moq.AutoMock
// Install: dotnet add package Moq.AutoMock
public class AutoMockTests
{
[Fact]
public async Task AutoMockAllDependencies()
{
var mocker = new AutoMocker();
// Get the service with all dependencies auto-mocked
var service = mocker.CreateInstance<OrderService>();
// Configure specific mocks
mocker.GetMock<IOrderRepository>()
.Setup(r => r.GetByIdAsync(It.IsAny<Guid>()))
.ReturnsAsync(new Order());
// Use the service
var result = await service.GetOrderAsync(Guid.NewGuid());
// Verify
mocker.GetMock<IOrderRepository>()
.Verify(r => r.GetByIdAsync(It.IsAny<Guid>()), Times.Once);
}
}
Mocking Common .NET Types
HttpClient
public class HttpClientMocking
{
[Fact]
public async Task MockHttpClient()
{
// Create a mock HttpMessageHandler
var mockHandler = new Mock<HttpMessageHandler>();
mockHandler.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("{\"id\": 1, \"name\": \"Test\"}")
});
var httpClient = new HttpClient(mockHandler.Object)
{
BaseAddress = new Uri("https://api.example.com")
};
// Use httpClient in your service
}
}
ILogger
public class LoggerMocking
{
[Fact]
public void VerifyLogging()
{
var mockLogger = new Mock<ILogger<OrderService>>();
var service = new OrderService(mockLogger.Object);
service.ProcessOrder(new Order());
// Verify log was called
mockLogger.Verify(
x => x.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((o, t) => o.ToString()!.Contains("Order processed")),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
}
}
DbContext (Entity Framework)
public class DbContextMocking
{
[Fact]
public async Task MockDbContext_WithInMemoryDatabase()
{
// Use in-memory database instead of mocking
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
using var context = new ApplicationDbContext(options);
// Seed data
context.Orders.Add(new Order { Id = Guid.NewGuid(), Total = 100m });
await context.SaveChangesAsync();
// Use in tests
var repository = new OrderRepository(context);
var orders = await repository.GetAllAsync();
Assert.Single(orders);
}
[Fact]
public async Task MockDbSet()
{
// For read-only scenarios, you can mock DbSet
var orders = new List<Order>
{
new Order { Id = Guid.NewGuid(), Total = 100m },
new Order { Id = Guid.NewGuid(), Total = 200m }
}.AsQueryable();
var mockSet = new Mock<DbSet<Order>>();
mockSet.As<IQueryable<Order>>().Setup(m => m.Provider).Returns(orders.Provider);
mockSet.As<IQueryable<Order>>().Setup(m => m.Expression).Returns(orders.Expression);
mockSet.As<IQueryable<Order>>().Setup(m => m.ElementType).Returns(orders.ElementType);
mockSet.As<IQueryable<Order>>().Setup(m => m.GetEnumerator()).Returns(orders.GetEnumerator());
var mockContext = new Mock<ApplicationDbContext>();
mockContext.Setup(c => c.Orders).Returns(mockSet.Object);
}
}
IConfiguration
public class ConfigurationMocking
{
[Fact]
public void MockConfiguration()
{
var configuration = Substitute.For<IConfiguration>();
// Mock indexer
configuration["ConnectionStrings:DefaultConnection"]
.Returns("Server=test;Database=test;");
// Mock GetSection
var section = Substitute.For<IConfigurationSection>();
section.Value.Returns("test-value");
configuration.GetSection("AppSettings:ApiKey").Returns(section);
// Or use in-memory configuration
var inMemorySettings = new Dictionary<string, string?>
{
{ "ConnectionStrings:DefaultConnection", "Server=test;" },
{ "AppSettings:ApiKey", "test-key" }
};
var realConfiguration = new ConfigurationBuilder()
.AddInMemoryCollection(inMemorySettings)
.Build();
}
}
Anti-Patterns
1. Over-Mocking
// BAD: Mocking everything
[Fact]
public void OverMocking_Brittle()
{
var mockA = new Mock<IDependencyA>();
var mockB = new Mock<IDependencyB>();
var mockC = new Mock<IDependencyC>();
var mockD = new Mock<IDependencyD>();
var mockE = new Mock<IDependencyE>();
// ... setup 20 methods ...
// Test is fragile and hard to maintain
}
// GOOD: Mock only what you need
[Fact]
public void FocusedMocking_Robust()
{
var mockRepository = new Mock<IOrderRepository>();
mockRepository.Setup(r => r.GetByIdAsync(It.IsAny<Guid>()))
.ReturnsAsync(new Order());
// Test focuses on one interaction
}
2. Testing Implementation Details
// BAD: Testing internal implementation
[Fact]
public void TestingImplementation_Fragile()
{
var mock = new Mock<IOrderRepository>();
var service = new OrderService(mock.Object);
service.Process(order);
// Fragile: tied to internal implementation
mock.Verify(r => r.BeginTransaction(), Times.Once);
mock.Verify(r => r.ValidateOrder(order), Times.Once);
mock.Verify(r => r.SaveOrder(order), Times.Once);
mock.Verify(r => r.CommitTransaction(), Times.Once);
}
// GOOD: Testing observable behavior
[Fact]
public void TestingBehavior_Robust()
{
var mock = new Mock<IOrderRepository>();
var service = new OrderService(mock.Object);
var result = service.Process(order);
Assert.True(result.IsSuccess);
mock.Verify(r => r.SaveAsync(order), Times.Once); // Only verify what matters
}
3. Mock Return Mock
// BAD: Mock returning another mock
[Fact]
public void MockReturningMock_Confusing()
{
var mockFactory = new Mock<IServiceFactory>();
var mockService = new Mock<IOrderService>();
var mockResult = new Mock<IOrderResult>();
mockFactory.Setup(f => f.CreateService()).Returns(mockService.Object);
mockService.Setup(s => s.Process(It.IsAny<Order>())).Returns(mockResult.Object);
mockResult.Setup(r => r.IsSuccess).Returns(true);
// Hard to understand and maintain
}
// GOOD: Use real objects where possible
[Fact]
public void UseRealObjects_Clear()
{
var mockRepository = new Mock<IOrderRepository>();
var service = new OrderService(mockRepository.Object);
var result = service.Process(new Order());
Assert.True(result.IsSuccess);
}
4. Verify Everything
// BAD: Verifying every single call
[Fact]
public void VerifyEverything_Noisy()
{
var mock = new Mock<IOrderRepository>();
var service = new OrderService(mock.Object);
service.Process(order);
mock.Verify(r => r.GetByIdAsync(It.IsAny<Guid>()), Times.Once);
mock.Verify(r => r.ValidateAsync(It.IsAny<Order>()), Times.Once);
mock.Verify(r => r.SaveAsync(It.IsAny<Order>()), Times.Once);
mock.Verify(r => r.PublishEventAsync(It.IsAny<OrderEvent>()), Times.Once);
mock.VerifyNoOtherCalls(); // Extremely brittle!
}
// GOOD: Verify only the important interactions
[Fact]
public void VerifyEssential_Clean()
{
var mock = new Mock<IOrderRepository>();
var service = new OrderService(mock.Object);
service.Process(order);
// Only verify the essential interaction for this test
mock.Verify(r => r.SaveAsync(It.Is<Order>(o => o.Status == OrderStatus.Processed)));
}
Interview Questions
1. Whatâs the difference between a mock and a stub?
Answer:
- Stub: Provides predetermined responses to method calls. Used to isolate the system under test by replacing dependencies. Verification focuses on the result.
- Mock: Records calls made to it and allows verification of interactions. Used when you need to verify that specific methods were called with specific arguments.
Example: Testing an email notification service
- Use a stub for the order repository (provides test data)
- Use a mock for the email service (verify notification was sent)
2. When would you use a fake instead of a mock?
Answer: Use a fake when:
- You need realistic behavior across multiple operations
- The mock setup would be too complex
- Youâre testing integration between components
- State needs to persist across method calls
Common examples:
- In-memory database instead of SQL Server mock
- In-memory file system for storage tests
- Fake message queue for async testing
3. What are the dangers of over-mocking?
Answer:
- Brittle tests: Tests break when implementation changes, even if behavior is correct
- False confidence: Tests pass but donât verify real behavior
- Maintenance burden: Complex mock setups are hard to maintain
- Missing integration bugs: Mocks hide real integration issues
- Testing the mocks: You end up verifying mock configuration, not actual code
4. How do you test code that depends on DateTime.Now?
Answer: Abstract time behind an interface or use TimeProvider (.NET 8+):
// Option 1: Custom interface
public interface ITimeProvider
{
DateTime Now { get; }
}
// Option 2: .NET 8 TimeProvider
public class MyService(TimeProvider timeProvider)
{
public bool IsExpired(DateTime expirationDate)
{
return timeProvider.GetUtcNow() > expirationDate;
}
}
// Test with fake time
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2024, 6, 15, 0, 0, 0, TimeSpan.Zero));
var service = new MyService(fakeTime);
5. Should you mock third-party libraries?
Answer: Generally avoid mocking types you donât own. Instead:
- Create wrapper interfaces for third-party dependencies
- Use integration tests with real libraries
- Use test fixtures provided by the library (if available)
Example for HttpClient:
// Create wrapper interface
public interface IApiClient
{
Task<T> GetAsync<T>(string url);
}
// Mock your interface, not HttpClient directly
var mock = Substitute.For<IApiClient>();
Key Takeaways
- Know your test doubles: Dummies, stubs, spies, mocks, and fakes serve different purposes
- Mock at boundaries: External services, databases, and non-deterministic dependencies
- Donât over-mock: Prefer real objects for value types and simple dependencies
- Verify behavior, not implementation: Focus on observable outcomes
- Use auto-mocking containers: Reduce boilerplate with AutoFixture or AutoMock
- Consider fakes for complex scenarios: When mock setup becomes unwieldy