Testing Guide: ConnectSoft API Library Template¶
The ConnectSoft API Library Template includes comprehensive testing support to ensure the reliability and correctness of your API client library. This guide covers testing strategies, mock server setup, unit testing, integration testing, and chaos testing.
Testing Strategy Overview¶
The template provides a multi-layered testing approach:
- Unit Tests: Test individual methods and components in isolation
- Mock Server Tests: Test with WireMock.Net to simulate API responses
- Integration Tests: Test end-to-end scenarios with real or mocked APIs
- Chaos Tests: Validate resiliency mechanisms under failure conditions
Unit Testing¶
Framework¶
The template uses MSTest as the testing framework, providing:
[TestClass]and[TestMethod]attributes[TestInitialize]and[TestCleanup]for setup/teardown- Assertion methods for validation
- Code coverage collection
Test Structure¶
[TestClass]
public class MyServiceUnitTests
{
private IConfiguration? configuration;
private IServiceCollection? services;
private IServiceProvider? serviceProvider;
private IMyService? myService;
[TestInitialize]
public void TestInitialize()
{
// Setup services and configuration
services = new ServiceCollection();
services.AddLogging(options => options.AddDebug());
var configBuilder = new ConfigurationBuilder();
configBuilder.AddJsonFile("appsettings.UnitTests.json", optional: true);
configuration = configBuilder.Build();
services.AddSingleton<IConfiguration>(configuration);
services.AddMyServiceOptions(configuration);
services.AddMyServiceMetrics(); // if UseMetrics=true
services.AddMyServiceService();
serviceProvider = services.BuildServiceProvider();
myService = serviceProvider.GetService<IMyService>();
}
[TestMethod]
public async Task GetDataAsync_WithValidRequest_ReturnsSuccess()
{
// Arrange
// Act
var result = await myService!.GetDataAsync();
// Assert
Assert.IsNotNull(result);
// Additional assertions
}
}
Test Configuration¶
Tests use appsettings.UnitTests.json for configuration:
{
"MyService": {
"BaseUrl": "https://api.test.example.com",
"DefaultTimeout": "00:00:10",
"ApiKeyAuthentication": {
"ApiKey": "test-api-key",
"HeaderName": "X-API-Key"
},
"EnableHttpStandardResilience": false,
"EnableChaosInjection": false
}
}
Best Practices¶
- Isolate Tests: Each test should be independent and not rely on other tests
- Arrange-Act-Assert: Follow AAA pattern for clarity
- Test Edge Cases: Test error conditions, null values, timeouts
- Use Descriptive Names: Test method names should describe what they test
Mock Server Testing with WireMock.Net¶
Overview¶
WireMock.Net allows you to simulate API responses without requiring the actual API, enabling reliable and fast integration tests.
Setup¶
The template includes WireMock.Net in the test project. To use it:
- Start WireMock Server: Create a WireMock server instance
- Configure Mock Responses: Define expected requests and responses
- Configure Service: Point your service to the mock server URL
- Run Tests: Execute tests against the mock server
Example: Mock Server Test¶
[TestClass]
public class MyServiceWithMockedServerUnitTests
{
private WireMockServer? mockServer;
private IConfiguration? configuration;
private IServiceCollection? services;
private IServiceProvider? serviceProvider;
private IMyService? myService;
[TestInitialize]
public void TestInitialize()
{
// Start WireMock server
mockServer = WireMockServer.Start();
// Configure mock responses
mockServer.Given(Request.Create()
.WithPath("/api/data")
.UsingGet())
.RespondWith(Response.Create()
.WithStatusCode(200)
.WithHeader("Content-Type", "application/json")
.WithBodyAsJson(new { data = "test", value = 123 }));
// Setup services with mock server URL
services = new ServiceCollection();
services.AddLogging(options => options.AddDebug());
var configBuilder = new ConfigurationBuilder();
configBuilder.AddJsonFile("appsettings.MockedServerUnitTests.json", optional: true);
configuration = configBuilder.Build();
// Override base URL to use mock server
services.AddSingleton<IConfiguration>(configuration);
services.AddMyServiceOptions(configuration);
services.OverrideBaseUrl(mockServer.Url!); // Use mock server URL
services.AddMyServiceMetrics();
services.AddMyServiceService();
serviceProvider = services.BuildServiceProvider();
myService = serviceProvider.GetService<IMyService>();
}
[TestCleanup]
public void TestCleanup()
{
mockServer?.Stop();
mockServer?.Dispose();
}
[TestMethod]
public async Task GetDataAsync_WithMockedServer_ReturnsSuccess()
{
// Arrange
// Act
var result = await myService!.GetDataAsync();
// Assert
Assert.IsNotNull(result);
Assert.AreEqual("test", result.Data);
// Verify mock server received the request
var requests = mockServer!.LogEntries;
Assert.AreEqual(1, requests.Count());
}
}
Mock Server Configuration¶
Tests use appsettings.MockedServerUnitTests.json:
{
"MyService": {
"BaseUrl": "http://localhost:5000", // Will be overridden by mock server URL
"DefaultTimeout": "00:00:10",
"ApiKeyAuthentication": {
"ApiKey": "test-api-key",
"HeaderName": "X-API-Key"
},
"EnableHttpStandardResilience": false,
"EnableChaosInjection": false
}
}
Mock Server Features¶
Request Matching¶
mockServer.Given(Request.Create()
.WithPath("/api/data")
.WithParam("id", "123")
.UsingGet())
.RespondWith(Response.Create()
.WithStatusCode(200)
.WithBodyAsJson(new { id = 123, name = "Test" }));
Response Stubbing¶
// Success response
mockServer.Given(Request.Create()
.WithPath("/api/success")
.UsingGet())
.RespondWith(Response.Create()
.WithStatusCode(200)
.WithBodyAsJson(new { success = true }));
// Error response
mockServer.Given(Request.Create()
.WithPath("/api/error")
.UsingGet())
.RespondWith(Response.Create()
.WithStatusCode(500)
.WithBodyAsJson(new { error = "Internal Server Error" }));
// Timeout simulation
mockServer.Given(Request.Create()
.WithPath("/api/slow")
.UsingGet())
.RespondWith(Response.Create()
.WithStatusCode(200)
.WithDelay(TimeSpan.FromSeconds(10))
.WithBodyAsJson(new { data = "slow" }));
Request Verification¶
[TestMethod]
public async Task GetDataAsync_VerifiesRequestHeaders()
{
// Act
await myService!.GetDataAsync();
// Assert
var requests = mockServer!.LogEntries;
var request = requests.First();
Assert.IsTrue(request.RequestMessage.Headers.ContainsKey("X-API-Key"));
Assert.AreEqual("test-api-key", request.RequestMessage.Headers["X-API-Key"].First());
}
Integration Testing¶
Overview¶
Integration tests validate end-to-end functionality with real or mocked APIs.
Real API Integration Tests¶
[TestClass]
public class MyServiceIntegrationTests
{
[TestMethod]
[TestCategory("Integration")]
public async Task GetDataAsync_WithRealAPI_ReturnsSuccess()
{
// Arrange
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.IntegrationTests.json")
.AddEnvironmentVariables()
.Build();
var services = new ServiceCollection();
services.AddLogging();
services.AddMyServiceOptions(configuration);
services.AddMyServiceService();
var serviceProvider = services.BuildServiceProvider();
var service = serviceProvider.GetRequiredService<IMyService>();
// Act
var result = await service.GetDataAsync();
// Assert
Assert.IsNotNull(result);
}
}
Mocked Integration Tests¶
Use WireMock.Net for integration tests when you can't access the real API:
[TestClass]
public class MyServiceMockedIntegrationTests
{
private WireMockServer? mockServer;
[TestInitialize]
public void Setup()
{
mockServer = WireMockServer.Start();
// Configure comprehensive mock scenarios
}
[TestMethod]
public async Task GetDataAsync_EndToEnd_HandlesAllScenarios()
{
// Test complete workflows with mocked API
}
}
Chaos Testing¶
Overview¶
Chaos testing validates that your library's resiliency mechanisms work correctly under failure conditions.
Enabling Chaos Injection¶
{
"MyService": {
"EnableChaosInjection": true,
"ChaosInjection": {
"InjectionRate": 0.1, // 10% of requests
"Latency": "00:00:05" // 5 second delay
},
"EnableHttpStandardResilience": true,
"HttpStandardResilience": {
"Retry": {
"MaxRetryAttempts": 3,
"Delay": "00:00:02"
},
"CircuitBreaker": {
"FailureRatio": 0.1,
"BreakDuration": "00:00:05"
}
}
}
}
Chaos Test Example¶
[TestClass]
public class MyServiceChaosTests
{
[TestMethod]
public async Task GetDataAsync_WithChaosInjection_RetriesOnFailure()
{
// Arrange
// Configure service with chaos injection enabled
// EnableHttpStandardResilience = true
// EnableChaosInjection = true
// InjectionRate = 0.5 (50% failure rate)
// Act
var result = await myService!.GetDataAsync();
// Assert
// Verify retry mechanism handled the chaos injection
// Verify circuit breaker behavior
// Verify timeout handling
}
}
Chaos Testing Scenarios¶
Latency Injection¶
Test timeout and retry mechanisms:
[TestMethod]
public async Task GetDataAsync_WithLatencyInjection_HandlesTimeout()
{
// Configure chaos injection with high latency
// Verify timeout mechanism works
// Verify retry attempts are made
}
Fault Injection¶
Test exception handling:
[TestMethod]
public async Task GetDataAsync_WithFaultInjection_HandlesExceptions()
{
// Configure chaos injection with fault injection
// Verify exceptions are caught and handled
// Verify custom exceptions are thrown
}
Outcome Injection¶
Test error response handling:
[TestMethod]
public async Task GetDataAsync_WithErrorInjection_RetriesOnErrors()
{
// Configure chaos injection with error responses
// Verify retry mechanism handles errors
// Verify circuit breaker opens after failures
}
Metrics Testing¶
When UseMetrics=true, the template generates metrics unit tests:
[TestClass]
public class MyServiceMetricsUnitTests
{
[TestMethod]
public void IncrementRequestCounter_IncrementsCounter()
{
// Arrange
var meterFactory = new TestMeterFactory();
var metrics = new MyServiceMetrics(meterFactory);
// Act
metrics.IncrementRequestCounter();
// Assert
// Verify counter was incremented
}
[TestMethod]
public void RecordRequestProcessingTime_RecordsDuration()
{
// Arrange
var meterFactory = new TestMeterFactory();
var metrics = new MyServiceMetrics(meterFactory);
// Act
metrics.RecordRequestProcessingTime(100.0);
// Assert
// Verify duration was recorded
}
}
Test Configuration Files¶
appsettings.UnitTests.json¶
Configuration for standard unit tests:
{
"MyService": {
"BaseUrl": "https://api.test.example.com",
"DefaultTimeout": "00:00:10",
"ApiKeyAuthentication": {
"ApiKey": "test-key",
"HeaderName": "X-API-Key"
},
"EnableHttpStandardResilience": false,
"EnableChaosInjection": false
}
}
appsettings.MockedServerUnitTests.json¶
Configuration for mock server tests:
{
"MyService": {
"BaseUrl": "http://localhost:5000",
"DefaultTimeout": "00:00:10",
"ApiKeyAuthentication": {
"ApiKey": "test-key",
"HeaderName": "X-API-Key"
},
"EnableHttpStandardResilience": true,
"HttpStandardResilience": {
"Retry": {
"MaxRetryAttempts": 2,
"Delay": "00:00:01"
}
},
"EnableChaosInjection": false
}
}
Code Coverage¶
The template includes code coverage collection:
Configuration¶
The .runsettings file configures code coverage:
<DataCollectionRunSettings>
<DataCollectors>
<DataCollector friendlyName="Code Coverage" />
</DataCollectors>
</DataCollectionRunSettings>
Running with Coverage¶
Coverage Thresholds¶
The CI/CD pipeline enforces code coverage thresholds (configurable via codeCoverageThreshold variable).
Testing Best Practices¶
Organization¶
- Test Structure: Mirror source structure in tests
- Test Categories: Use
[TestCategory]for grouping tests - Test Naming: Use descriptive names that explain what is tested
Isolation¶
- Independent Tests: Each test should be independent
- Clean State: Reset state between tests
- Mock External Dependencies: Mock HTTP clients, external services
Coverage¶
- Happy Path: Test successful scenarios
- Error Cases: Test error handling and exceptions
- Edge Cases: Test boundary conditions, null values, timeouts
- Resiliency: Test retry, circuit breaker, timeout mechanisms
Performance¶
- Fast Tests: Keep unit tests fast (< 100ms)
- Async Tests: Use async/await for async operations
- Mock External Calls: Avoid real network calls in unit tests
Troubleshooting¶
Tests Failing¶
Problem: Tests fail unexpectedly.
Solutions:
- Check test configuration files are correct
- Verify mock server is running (for mock server tests)
- Check service registration is correct
- Verify options are configured properly
Mock Server Not Working¶
Problem: Mock server doesn't respond or requests don't match.
Solutions:
- Verify mock server is started before tests
- Check request matching criteria
- Verify base URL is overridden correctly
- Check mock server logs for request details
Code Coverage Low¶
Problem: Code coverage is below threshold.
Solutions:
- Add more test cases
- Test error paths and edge cases
- Test all service methods
- Test metrics (if UseMetrics=true)
Conclusion¶
The ConnectSoft API Library Template provides comprehensive testing support to ensure your API client library is reliable and correct. By following the testing strategies and best practices in this guide, you can build robust test suites that validate your library's functionality and resiliency.
For more information, see:
- Resiliency Guide - Chaos testing details
- Configuration Guide - Test configuration
- Development Guide - Local testing workflow