đŸ§Ș

Mocking and Test Doubles

Testing Intermediate 4 min read 700 words

Comprehensive guide to test doubles, mocking frameworks, and best practices for .NET testing

Testing

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

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:

  1. Brittle tests: Tests break when implementation changes, even if behavior is correct
  2. False confidence: Tests pass but don’t verify real behavior
  3. Maintenance burden: Complex mock setups are hard to maintain
  4. Missing integration bugs: Mocks hide real integration issues
  5. 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:

  1. Create wrapper interfaces for third-party dependencies
  2. Use integration tests with real libraries
  3. 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

  1. Know your test doubles: Dummies, stubs, spies, mocks, and fakes serve different purposes
  2. Mock at boundaries: External services, databases, and non-deterministic dependencies
  3. Don’t over-mock: Prefer real objects for value types and simple dependencies
  4. Verify behavior, not implementation: Focus on observable outcomes
  5. Use auto-mocking containers: Reduce boilerplate with AutoFixture or AutoMock
  6. Consider fakes for complex scenarios: When mock setup becomes unwieldy

Further Reading

📚 Related Articles