Mocking Strategies in ConnectSoft Microservice Template¶
Purpose & Overview¶
Mocking Strategies in the ConnectSoft Microservice Template provide techniques for isolating units under test by replacing dependencies with test doubles. Effective mocking enables fast, reliable unit tests while maintaining confidence in code correctness. The template uses Moq as the primary mocking framework, supporting various test double patterns including mocks, stubs, fakes, and spies.
Mocking strategies provide:
- Isolation: Test units in isolation without external dependencies
- Speed: Fast test execution without I/O operations
- Reliability: Deterministic tests that don't depend on external systems
- Flexibility: Control dependencies' behavior for various test scenarios
- Verification: Verify interactions between components
- Testability: Enable testing of complex scenarios and edge cases
Mocking Philosophy
Mocking is a tool for achieving test isolation, not a goal in itself. The template emphasizes using appropriate test doubles for the right scenarios—mocks for verifying interactions, stubs for providing data, fakes for simplified implementations, and spies for observing behavior. Choose the simplest test double that satisfies the test requirements.
Test Double Types¶
Test Double Hierarchy¶
Test Double (Generic term)
├── Dummy (not used, just to fill parameters)
├── Stub (provides predefined responses)
├── Spy (records interactions for verification)
├── Mock (expects specific calls and verifies them)
└── Fake (working implementation with limitations)
Mocks¶
Purpose: Verify interactions and enforce expectations.
Characteristics: - Expects specific method calls - Verifies call counts and parameters - Fails if expectations aren't met - Used for behavior verification
Example:
// Arrange
var mockRepository = new Mock<IMicroserviceAggregateRootsRepository>();
mockRepository.Setup(r => r.Insert(It.IsAny<IMicroserviceAggregateRoot>()))
.Verifiable();
var processor = new DefaultMicroserviceAggregateRootsProcessor(
logger, timeProvider, mockRepository.Object, unitOfWork, eventBus, validator, metrics);
// Act
await processor.CreateMicroserviceAggregateRoot(input);
// Assert
mockRepository.Verify(r => r.Insert(It.IsAny<IMicroserviceAggregateRoot>()), Times.Once);
Stubs¶
Purpose: Provide predefined responses without verification.
Characteristics: - Returns fixed values or executes simple logic - No verification of interactions - Used for state verification - Focuses on output, not behavior
Example:
// Arrange
var stubRepository = new Mock<IMicroserviceAggregateRootsRepository>();
stubRepository.Setup(r => r.GetByIdAsync(It.IsAny<Guid>()))
.ReturnsAsync(new MicroserviceAggregateRootEntity
{
ObjectId = testId,
SomeValue = "Test Value"
});
var processor = new DefaultMicroserviceAggregateRootsProcessor(
logger, timeProvider, stubRepository.Object, unitOfWork, eventBus, validator, metrics);
// Act
var result = await processor.GetMicroserviceAggregateRootDetails(input);
// Assert
Assert.AreEqual("Test Value", result.SomeValue);
Fakes¶
Purpose: Provide working implementations with limitations.
Characteristics: - Real implementation with simplified behavior - Suitable for integration-style tests - May have limitations (e.g., in-memory storage) - Can be shared across multiple tests
Example:
// Fake implementation
public class InMemoryMicroserviceAggregateRootsRepository : IMicroserviceAggregateRootsRepository
{
private readonly List<IMicroserviceAggregateRoot> entities = new();
public Task<IMicroserviceAggregateRoot?> GetByIdAsync(Guid id, CancellationToken token = default)
{
return Task.FromResult(entities.FirstOrDefault(e => e.ObjectId == id));
}
public Task InsertAsync(IMicroserviceAggregateRoot entity, CancellationToken token = default)
{
entities.Add(entity);
return Task.CompletedTask;
}
}
// Usage in tests
[TestMethod]
public async Task Create_Should_Persist_Entity()
{
// Arrange
var fakeRepository = new InMemoryMicroserviceAggregateRootsRepository();
var processor = new DefaultMicroserviceAggregateRootsProcessor(
logger, timeProvider, fakeRepository, unitOfWork, eventBus, validator, metrics);
// Act
var result = await processor.CreateMicroserviceAggregateRoot(input);
// Assert
var retrieved = await fakeRepository.GetByIdAsync(result.ObjectId);
Assert.IsNotNull(retrieved);
}
Spies¶
Purpose: Record interactions for later verification.
Characteristics: - Records method calls and parameters - Allows verification after execution - More flexible than strict mocks - Useful for complex interaction verification
Example:
// Arrange
var spyRepository = new Mock<IMicroserviceAggregateRootsRepository>();
var calledMethods = new List<string>();
var callParameters = new List<object>();
spyRepository.Setup(r => r.Insert(It.IsAny<IMicroserviceAggregateRoot>()))
.Callback<IMicroserviceAggregateRoot>(entity =>
{
calledMethods.Add(nameof(IMicroserviceAggregateRootsRepository.Insert));
callParameters.Add(entity);
});
// Act
await processor.CreateMicroserviceAggregateRoot(input);
// Assert
Assert.AreEqual(1, calledMethods.Count);
Assert.AreEqual(nameof(IMicroserviceAggregateRootsRepository.Insert), calledMethods[0]);
Assert.IsInstanceOfType(callParameters[0], typeof(IMicroserviceAggregateRoot));
Moq Framework¶
Basic Usage¶
Creating Mocks:
// Full mock
var mock = new Mock<IMicroserviceAggregateRootsRepository>();
// Lightweight mock (using Mock.Of<T>)
var repository = Mock.Of<IMicroserviceAggregateRootsRepository>();
Setting Up Behavior:
// Return value
mock.Setup(r => r.GetByIdAsync(It.IsAny<Guid>()))
.ReturnsAsync(new MicroserviceAggregateRootEntity());
// Return value with parameters
mock.Setup(r => r.GetByIdAsync(It.Is<Guid>(id => id == testId)))
.ReturnsAsync(expectedEntity);
// Throw exception
mock.Setup(r => r.GetByIdAsync(It.IsAny<Guid>()))
.ThrowsAsync(new NotFoundException());
// Async method with cancellation token
mock.Setup(r => r.GetByIdAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new MicroserviceAggregateRootEntity());
Verifying Interactions:
// Verify method was called
mock.Verify(r => r.Insert(It.IsAny<IMicroserviceAggregateRoot>()), Times.Once);
// Verify method was never called
mock.Verify(r => r.Delete(It.IsAny<Guid>()), Times.Never);
// Verify method was called with specific parameters
mock.Verify(r => r.GetByIdAsync(It.Is<Guid>(id => id == testId)), Times.Once);
// Verify all setups
mock.VerifyAll();
Advanced Moq Patterns¶
Property Setup¶
// Setup property getter
mock.Setup(r => r.Count)
.Returns(10);
// Setup property setter
mock.SetupSet(r => r.Name = "Test")
.Verifiable();
// Setup property with backing field
mock.SetupProperty(r => r.Name, "Initial Value");
Callback¶
// Capture parameters
IMicroserviceAggregateRoot? capturedEntity = null;
mock.Setup(r => r.Insert(It.IsAny<IMicroserviceAggregateRoot>()))
.Callback<IMicroserviceAggregateRoot>(entity => capturedEntity = entity);
// Execute custom logic
mock.Setup(r => r.Insert(It.IsAny<IMicroserviceAggregateRoot>()))
.Callback<IMicroserviceAggregateRoot>(entity =>
{
// Custom validation or logging
Assert.IsNotNull(entity);
});
Sequences¶
// Return different values on subsequent calls
mock.SetupSequence(r => r.GetByIdAsync(It.IsAny<Guid>()))
.ReturnsAsync(entity1)
.ReturnsAsync(entity2)
.ThrowsAsync(new NotFoundException());
Protected Members¶
// Mock protected methods (requires Moq.Protected)
var mockHttpHandler = new Mock<HttpMessageHandler>();
mockHttpHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage { StatusCode = HttpStatusCode.OK });
Mocking Patterns by Component¶
Repository Mocking¶
Unit Testing Processors:
[TestMethod]
public async Task Create_WhenAlreadyExists_ThrowsException()
{
// Arrange
var mockRepository = new Mock<IMicroserviceAggregateRootsRepository>();
mockRepository.Setup(r => r.GetByIdAsync(It.IsAny<Guid>()))
.ReturnsAsync(new MicroserviceAggregateRootEntity()); // Entity exists
var mockUnitOfWork = new Mock<IUnitOfWork>();
mockUnitOfWork.Setup(u => u.ExecuteTransactional(It.IsAny<Action>()))
.Callback<Action>(action => action());
var processor = new DefaultMicroserviceAggregateRootsProcessor(
Mock.Of<ILogger<DefaultMicroserviceAggregateRootsProcessor>>(),
TimeProvider.System,
mockRepository.Object,
mockUnitOfWork.Object,
Mock.Of<IEventBus>(),
Mock.Of<IValidator<CreateMicroserviceAggregateRootInput>>(),
Mock.Of<IValidator<DeleteMicroserviceAggregateRootInput>>(),
Mock.Of<MicroserviceTemplateMetrics>());
var input = new CreateMicroserviceAggregateRootInput
{
ObjectId = Guid.NewGuid()
};
// Act & Assert
await Assert.ThrowsExceptionAsync<MicroserviceAggregateRootAlreadyExistsException>(
() => processor.CreateMicroserviceAggregateRoot(input));
}
Verifying Repository Calls:
[TestMethod]
public async Task Create_Should_Insert_Into_Repository()
{
// Arrange
var mockRepository = new Mock<IMicroserviceAggregateRootsRepository>();
mockRepository.Setup(r => r.GetByIdAsync(It.IsAny<Guid>()))
.ReturnsAsync((IMicroserviceAggregateRoot?)null); // Entity doesn't exist
var processor = new DefaultMicroserviceAggregateRootsProcessor(
logger, timeProvider, mockRepository.Object, unitOfWork, eventBus, validator, metrics);
// Act
var result = await processor.CreateMicroserviceAggregateRoot(input);
// Assert
mockRepository.Verify(r => r.Insert(It.IsAny<IMicroserviceAggregateRoot>()), Times.Once);
mockRepository.Verify(r => r.GetByIdAsync(It.IsAny<Guid>()), Times.Once);
}
Service Mocking¶
Mocking External Services:
[TestMethod]
public async Task ProcessPayment_WithValidRequest_ShouldSucceed()
{
// Arrange
var mockPaymentService = new Mock<IPaymentService>();
mockPaymentService.Setup(s => s.ProcessPaymentAsync(It.IsAny<PaymentRequest>()))
.ReturnsAsync(new PaymentResult
{
Success = true,
TransactionId = "txn-123"
});
var processor = new PaymentProcessor(
mockPaymentService.Object,
Mock.Of<ILogger<PaymentProcessor>>());
// Act
var result = await processor.ProcessPayment(new PaymentInput { Amount = 100 });
// Assert
Assert.IsTrue(result.Success);
Assert.AreEqual("txn-123", result.TransactionId);
}
Mocking Logger:
// Using Mock.Of<T> for simple cases
var logger = Mock.Of<ILogger<MyService>>();
// Using Mock<T> for verification
var mockLogger = new Mock<ILogger<MyService>>();
var logMessages = new List<string>();
mockLogger.Setup(l => l.Log(
It.IsAny<LogLevel>(),
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()))
.Callback<LogLevel, EventId, object, Exception, Func<object, Exception?, string>>(
(level, eventId, state, ex, formatter) =>
{
logMessages.Add(formatter(state, ex));
});
// Verify logging
mockLogger.Verify(
x => x.Log(
LogLevel.Error,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => true),
It.IsAny<Exception>(),
It.Is<Func<It.IsAnyType, Exception?, string>>((v, t) => true)),
Times.Once);
HTTP Client Mocking¶
Mocking HttpMessageHandler:
[TestMethod]
public async Task GetData_WithValidResponse_ShouldReturnData()
{
// Arrange
var mockHttpHandler = new Mock<HttpMessageHandler>();
mockHttpHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.Is<HttpRequestMessage>(req => req.Method == HttpMethod.Get),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("{\"data\": \"test\"}", Encoding.UTF8, "application/json")
});
var httpClient = new HttpClient(mockHttpHandler.Object);
var service = new ExternalServiceClient(httpClient, Mock.Of<ILogger<ExternalServiceClient>>());
// Act
var result = await service.GetDataAsync();
// Assert
Assert.AreEqual("test", result.Data);
}
Mocking IHttpClientFactory:
[TestMethod]
public async Task ProcessRequest_ShouldUseHttpClient()
{
// Arrange
var mockHandler = new Mock<HttpMessageHandler>();
mockHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage { StatusCode = HttpStatusCode.OK });
var mockHttpClientFactory = new Mock<IHttpClientFactory>();
mockHttpClientFactory.Setup(f => f.CreateClient(It.IsAny<string>()))
.Returns(new HttpClient(mockHandler.Object));
var service = new MyService(
mockHttpClientFactory.Object,
Mock.Of<ILogger<MyService>>());
// Act
await service.ProcessRequestAsync();
// Assert
mockHttpClientFactory.Verify(f => f.CreateClient("MyService"), Times.Once);
}
Message Bus Mocking¶
Mocking Event Bus:
[TestMethod]
public async Task Create_Should_Publish_Event()
{
// Arrange
var mockEventBus = new Mock<IEventBus>();
var publishedEvents = new List<object>();
mockEventBus.Setup(b => b.PublishEvent(
It.IsAny<MicroserviceAggregateRootCreatedEvent>(),
It.IsAny<CancellationToken>()))
.Callback<MicroserviceAggregateRootCreatedEvent, CancellationToken>((evt, ct) =>
{
publishedEvents.Add(evt);
})
.Returns(Task.CompletedTask);
var processor = new DefaultMicroserviceAggregateRootsProcessor(
logger, timeProvider, repository, unitOfWork, mockEventBus.Object, validator, metrics);
// Act
await processor.CreateMicroserviceAggregateRoot(input);
// Assert
Assert.AreEqual(1, publishedEvents.Count);
Assert.IsInstanceOfType(publishedEvents[0], typeof(MicroserviceAggregateRootCreatedEvent));
mockEventBus.Verify(b => b.PublishEvent(
It.IsAny<MicroserviceAggregateRootCreatedEvent>(),
It.IsAny<CancellationToken>()), Times.Once);
}
Configuration Mocking¶
Mocking IConfiguration:
[TestMethod]
public void GetConfiguration_ShouldReturnValue()
{
// Arrange
var mockConfiguration = new Mock<IConfiguration>();
mockConfiguration.Setup(c => c["Microservice:MicroserviceName"])
.Returns("TestService");
var service = new ConfigurationService(mockConfiguration.Object);
// Act
var name = service.GetServiceName();
// Assert
Assert.AreEqual("TestService", name);
}
// Alternative: Use in-memory configuration
[TestMethod]
public void GetConfiguration_WithInMemoryConfig_ShouldReturnValue()
{
// Arrange
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["Microservice:MicroserviceName"] = "TestService"
})
.Build();
var service = new ConfigurationService(config);
// Act
var name = service.GetServiceName();
// Assert
Assert.AreEqual("TestService", name);
}
Options Mocking¶
Mocking IOptions
[TestMethod]
public void GetOptions_ShouldReturnConfiguredValue()
{
// Arrange
var options = new MicroserviceOptions
{
MicroserviceName = "TestService",
StartupWarmupSeconds = 30
};
var mockOptions = new Mock<IOptions<MicroserviceOptions>>();
mockOptions.Setup(o => o.Value)
.Returns(options);
var service = new MyService(mockOptions.Object);
// Act
var name = service.GetServiceName();
// Assert
Assert.AreEqual("TestService", name);
}
Integration Testing Mocking¶
Mocking in WebApplicationFactory¶
Replace Services Per Test:
[TestMethod]
public async Task Test_WithMockedService()
{
var mockService = new Mock<IExternalService>();
mockService.Setup(s => s.GetData())
.ReturnsAsync("Test Data");
var client = this.factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
services.RemoveAll<IExternalService>();
services.AddSingleton(mockService.Object);
});
}).CreateClient();
// Test with mocked service
var response = await client.GetAsync("/api/data");
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("Test Data", content);
}
Mocking HTTP Clients in Integration Tests:
[TestMethod]
public async Task Test_WithMockedHttpClient()
{
var mockHttpHandler = new Mock<HttpMessageHandler>();
mockHttpHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("{\"data\": \"test\"}")
});
var client = this.factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
services.RemoveAll<IHttpClientFactory>();
services.AddSingleton<IHttpClientFactory>(sp =>
{
var factory = new Mock<IHttpClientFactory>();
factory.Setup(f => f.CreateClient(It.IsAny<string>()))
.Returns(new HttpClient(mockHttpHandler.Object));
return factory.Object;
});
});
}).CreateClient();
// Test with mocked HTTP client
}
Best Practices¶
Do's¶
-
Use Appropriate Test Doubles
// ✅ GOOD - Use stub for data, mock for verification var stubRepository = new Mock<IMicroserviceAggregateRootsRepository>(); stubRepository.Setup(r => r.GetByIdAsync(It.IsAny<Guid>())) .ReturnsAsync(entity); var mockEventBus = new Mock<IEventBus>(); // ... setup ... mockEventBus.Verify(b => b.PublishEvent(...), Times.Once); -
Use Mock.Of
for Simple Cases -
Verify Important Interactions
-
Isolate Test Setup
// ✅ GOOD - Clear, focused setup [TestInitialize] public void Setup() { this.mockRepository = new Mock<IMicroserviceAggregateRootsRepository>(); this.processor = new DefaultMicroserviceAggregateRootsProcessor( Mock.Of<ILogger<...>>(), TimeProvider.System, this.mockRepository.Object, // ... other dependencies ); } -
Use Real Implementations When Possible
Don'ts¶
-
Don't Over-Mock
// ❌ BAD - Mocking everything var mockLogger = new Mock<ILogger<MyService>>(); var mockTimeProvider = new Mock<TimeProvider>(); var mockValidator = new Mock<IValidator<Input>>(); // ✅ GOOD - Mock only what's necessary var logger = Mock.Of<ILogger<MyService>>(); var timeProvider = TimeProvider.System; var validator = new InputValidator(); -
Don't Verify Unnecessary Interactions
-
Don't Use Mocks for Simple Value Objects
-
Don't Share Mock State Between Tests
-
Don't Mock What You're Testing
Common Patterns¶
Test Fixtures¶
Creating Reusable Test Fixtures:
public class ProcessorTestFixture
{
public Mock<IMicroserviceAggregateRootsRepository> MockRepository { get; }
public Mock<IUnitOfWork> MockUnitOfWork { get; }
public Mock<IEventBus> MockEventBus { get; }
public DefaultMicroserviceAggregateRootsProcessor Processor { get; }
public ProcessorTestFixture()
{
this.MockRepository = new Mock<IMicroserviceAggregateRootsRepository>();
this.MockUnitOfWork = new Mock<IUnitOfWork>();
this.MockEventBus = new Mock<IEventBus>();
this.Processor = new DefaultMicroserviceAggregateRootsProcessor(
Mock.Of<ILogger<DefaultMicroserviceAggregateRootsProcessor>>(),
TimeProvider.System,
this.MockRepository.Object,
this.MockUnitOfWork.Object,
this.MockEventBus.Object,
Mock.Of<IValidator<CreateMicroserviceAggregateRootInput>>(),
Mock.Of<IValidator<DeleteMicroserviceAggregateRootInput>>(),
Mock.Of<MicroserviceTemplateMetrics>());
}
public void SetupEntityExists(Guid id)
{
this.MockRepository.Setup(r => r.GetByIdAsync(id))
.ReturnsAsync(new MicroserviceAggregateRootEntity { ObjectId = id });
}
public void SetupEntityNotFound(Guid id)
{
this.MockRepository.Setup(r => r.GetByIdAsync(id))
.ReturnsAsync((IMicroserviceAggregateRoot?)null);
}
}
Builder Pattern for Test Data¶
public class CreateMicroserviceAggregateRootInputBuilder
{
private Guid objectId = Guid.NewGuid();
public CreateMicroserviceAggregateRootInputBuilder WithObjectId(Guid id)
{
this.objectId = id;
return this;
}
public CreateMicroserviceAggregateRootInput Build()
{
return new CreateMicroserviceAggregateRootInput
{
ObjectId = this.objectId
};
}
}
// Usage
var input = new CreateMicroserviceAggregateRootInputBuilder()
.WithObjectId(testId)
.Build();
Troubleshooting¶
Issue: Mock Setup Not Working¶
Symptoms: Mock returns null or default values instead of configured behavior.
Solutions:
1. Verify mock setup is before the method call
2. Check parameter matching (use It.IsAny<T>() if unsure)
3. Ensure async methods use ReturnsAsync() instead of Returns()
4. Verify mock object is passed correctly (use .Object property)
Issue: Verification Failing¶
Symptoms: Verify() calls fail unexpectedly.
Solutions:
1. Check call count expectations (use Times.AtLeast() if unsure)
2. Verify parameter matching (exact match vs It.IsAny<>())
3. Ensure method was actually called (check for exceptions)
4. Use VerifyAll() to see all verification failures
Issue: Protected Members Not Mockable¶
Symptoms: Can't mock protected methods or properties.
Solutions:
1. Use Moq.Protected namespace
2. Use ItExpr instead of It for parameter matching
3. Use string method name for protected methods
4. Consider exposing protected members through public interface if needed
Issue: Mock State Between Tests¶
Symptoms: Tests fail when run together but pass individually.
Solutions:
1. Create fresh mocks in [TestInitialize] or test method
2. Use Mock.Reset() between tests if needed
3. Avoid static mock fields
4. Use [TestCleanup] to reset state if necessary
Summary¶
Mocking strategies in the ConnectSoft Microservice Template provide:
- ✅ Test Isolation: Isolate units under test from dependencies
- ✅ Fast Tests: Execute tests without I/O operations
- ✅ Flexible Testing: Test various scenarios and edge cases
- ✅ Interaction Verification: Verify component interactions
- ✅ Multiple Patterns: Support for mocks, stubs, fakes, and spies
- ✅ Moq Integration: Comprehensive Moq framework support
- ✅ Best Practices: Guidelines for effective mocking
By following these patterns, teams can:
- Write Reliable Tests: Create tests that don't depend on external systems
- Test Edge Cases: Easily simulate error conditions and unusual scenarios
- Verify Interactions: Ensure components interact correctly
- Maintain Testability: Keep code testable through dependency injection
- Speed Up Tests: Run tests quickly without external dependencies
Effective mocking strategies ensure that tests are fast, reliable, and maintainable, enabling teams to achieve high code coverage and confidence in their codebase while maintaining clean architecture principles.