Skip to content

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:

  1. Unit Tests: Test individual methods and components in isolation
  2. Mock Server Tests: Test with WireMock.Net to simulate API responses
  3. Integration Tests: Test end-to-end scenarios with real or mocked APIs
  4. 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

  1. Isolate Tests: Each test should be independent and not rely on other tests
  2. Arrange-Act-Assert: Follow AAA pattern for clarity
  3. Test Edge Cases: Test error conditions, null values, timeouts
  4. 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:

  1. Start WireMock Server: Create a WireMock server instance
  2. Configure Mock Responses: Define expected requests and responses
  3. Configure Service: Point your service to the mock server URL
  4. 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

dotnet test --collect:"XPlat Code Coverage" --settings MyService.runsettings

Coverage Thresholds

The CI/CD pipeline enforces code coverage thresholds (configurable via codeCoverageThreshold variable).

Testing Best Practices

Organization

  1. Test Structure: Mirror source structure in tests
  2. Test Categories: Use [TestCategory] for grouping tests
  3. Test Naming: Use descriptive names that explain what is tested

Isolation

  1. Independent Tests: Each test should be independent
  2. Clean State: Reset state between tests
  3. Mock External Dependencies: Mock HTTP clients, external services

Coverage

  1. Happy Path: Test successful scenarios
  2. Error Cases: Test error handling and exceptions
  3. Edge Cases: Test boundary conditions, null values, timeouts
  4. Resiliency: Test retry, circuit breaker, timeout mechanisms

Performance

  1. Fast Tests: Keep unit tests fast (< 100ms)
  2. Async Tests: Use async/await for async operations
  3. 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: