Skip to content

Integration Testing in ConnectSoft Microservice Template

Purpose & Overview

Integration Testing in the ConnectSoft Microservice Template validates that multiple components of the microservice work together correctly, testing the complete application stack from HTTP endpoints through to persistence and external services. Integration tests verify real interactions between layers, ensuring that the application functions correctly as a cohesive system.

Why Integration Testing?

Integration testing provides several critical benefits:

  • End-to-End Validation: Tests complete user journeys through the entire application stack
  • Real Component Interaction: Validates actual interactions between controllers, services, repositories, and databases
  • Configuration Validation: Ensures configuration works correctly in realistic scenarios
  • Middleware Pipeline: Tests authentication, authorization, logging, and other middleware
  • Database Integration: Validates data persistence and retrieval with real database operations
  • API Contract Testing: Verifies REST, gRPC, GraphQL, and SignalR endpoints work correctly
  • Error Handling: Tests error paths and exception handling across layers
  • Performance Insights: Provides realistic performance characteristics

Integration Testing Philosophy

Integration tests bridge the gap between unit tests (isolated components) and end-to-end tests (full system). They test multiple components together in a controlled environment, providing confidence that the application works correctly as an integrated system while remaining fast enough to run frequently.

Architecture Overview

Integration Testing Stack

Integration Test
WebApplicationFactory / TestServer
ASP.NET Core Application
    ├── Middleware Pipeline
    ├── Controllers / gRPC Services / GraphQL / SignalR
    ├── Application Services (Processors/Retrievers)
    ├── Domain Logic
    ├── Repository Layer
    └── Database (In-Memory / Test Container / Test Database)

Test Project Structure

IntegrationTests Project
├── Controllers/
│   ├── MicroserviceAggregateRootsControllerTests.cs
│   └── HealthChecksControllerTests.cs
├── Grpc/
│   └── GrpcServiceTests.cs
├── GraphQL/
│   └── GraphQLQueryTests.cs
├── SignalR/
│   └── SignalRHubTests.cs
├── Database/
│   └── RepositoryIntegrationTests.cs
├── Fixtures/
│   └── WebApplicationFactoryFixture.cs
└── Helpers/
    ├── TestDataBuilder.cs
    └── DatabaseSeeder.cs

Testing Approaches

Approach Tool Use Case
WebApplicationFactory ASP.NET Core Full application stack testing with HTTP client
TestServer ASP.NET Core In-memory testing with direct HTTP access
Test Containers Testcontainers Real database testing in containers
In-Memory Database EF Core / NHibernate Fast database testing without external dependencies

WebApplicationFactory

Purpose

WebApplicationFactory<TEntryPoint> creates a test host for the application, allowing integration tests to run the full application stack in memory. It provides an HttpClient that can make requests to the application without network overhead.

Basic Usage

using Microsoft.AspNetCore.Mvc.Testing;

[TestClass]
public class MicroserviceAggregateRootsControllerTests 
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> factory;

    public MicroserviceAggregateRootsControllerTests(
        WebApplicationFactory<Program> factory)
    {
        this.factory = factory;
    }

    [TestMethod]
    public async Task GetMicroserviceAggregateRoot_Should_Return_Ok()
    {
        // Arrange
        var client = this.factory.CreateClient();

        // Act
        var response = await client.GetAsync("/api/microserviceAggregateRoots/{id}");

        // Assert
        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadAsStringAsync();
        Assert.IsNotNull(content);
    }
}

Creating Custom Factory

Custom Factory with Configuration Overrides:

public class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureAppConfiguration((context, config) =>
        {
            config.AddInMemoryCollection(new Dictionary<string, string>
            {
                ["Microservice:MicroserviceName"] = "TestService",
                ["Microservice:StartupWarmupSeconds"] = "0",
                ["ConnectionStrings:DefaultConnection"] = "Server=localhost;Database=TestDb;..."
            });
        });

        builder.ConfigureServices(services =>
        {
            // Replace services for testing
            services.RemoveAll<IMicroserviceAggregateRootsRepository>();
            services.AddScoped<IMicroserviceAggregateRootsRepository, 
                InMemoryMicroserviceAggregateRootsRepository>();
        });
    }
}

Using Custom Factory:

[TestClass]
public class MyControllerTests : IClassFixture<CustomWebApplicationFactory>
{
    private readonly CustomWebApplicationFactory factory;

    public MyControllerTests(CustomWebApplicationFactory factory)
    {
        this.factory = factory;
    }

    [TestMethod]
    public async Task Test_WithCustomConfiguration()
    {
        var client = this.factory.CreateClient();
        // Test with custom configuration
    }
}

Configuration Overrides

Override Configuration Per Test:

[TestMethod]
public async Task Test_WithSpecificConfiguration()
{
    var client = this.factory.WithWebHostBuilder(builder =>
    {
        builder.ConfigureAppConfiguration((context, config) =>
        {
            config.AddInMemoryCollection(new Dictionary<string, string>
            {
                ["FeatureManagement:UseNewFeature"] = "true"
            });
        });
    }).CreateClient();

    // Test with feature enabled
    var response = await client.GetAsync("/api/feature");
    response.EnsureSuccessStatusCode();
}

Override 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);
}

TestServer

Purpose

TestServer provides an in-memory test host that allows direct access to the application without network overhead. It's ideal for BDD testing, acceptance tests, and scenarios where you need direct access to the application's services.

Basic Usage

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;

[TestClass]
public class TestServerTests
{
    private TestServer? server;
    private HttpClient? client;

    [TestInitialize]
    public void Setup()
    {
        var hostBuilder = Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<TestStartup>();
                webBuilder.UseTestServer();
            });

        var host = hostBuilder.Build();
        host.Start();

        this.server = host.GetTestServer();
        this.client = this.server.CreateClient();
    }

    [TestCleanup]
    public void Cleanup()
    {
        this.client?.Dispose();
        this.server?.Dispose();
    }

    [TestMethod]
    public async Task HealthCheck_Should_Return_Healthy()
    {
        // Act
        var response = await this.client!.GetAsync("/health");

        // Assert
        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadAsStringAsync();
        Assert.Contains("Healthy", content);
    }
}

Creating Test Host

Custom Test Host Builder:

private static IHostBuilder CreateHostBuilder() =>
    Host.CreateDefaultBuilder()
        .ConfigureLogging(logging =>
        {
            logging.ClearProviders();
            logging.AddConsole();
            logging.SetMinimumLevel(LogLevel.Warning);
        })
        .ConfigureAppConfiguration((hostBuilderContext, configurationBuilder) =>
        {
            configurationBuilder
                .SetBasePath(hostBuilderContext.HostingEnvironment.ContentRootPath)
                .AddJsonFile("appsettings.Development.json", optional: true)
                .AddInMemoryCollection(new Dictionary<string, string>
                {
                    ["Microservice:StartupWarmupSeconds"] = "0"
                })
                .AddEnvironmentVariables();
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<TestStartup>();
            webBuilder.UseTestServer();
        });

Accessing Services

Get Services from TestServer:

[TestMethod]
public async Task Test_WithServiceAccess()
{
    var host = CreateHostBuilder().Build();
    host.Start();

    var server = host.GetTestServer();
    var client = server.CreateClient();

    // Access services from DI container
    var repository = host.Services.GetRequiredService<IMicroserviceAggregateRootsRepository>();
    var processor = host.Services.GetRequiredService<IMicroserviceAggregateRootsProcessor>();

    // Setup test data
    var entity = new MicroserviceAggregateRootEntity { ObjectId = Guid.NewGuid() };
    await repository.InsertAsync(entity);

    // Test endpoint
    var response = await client.GetAsync($"/api/microserviceAggregateRoots/{entity.ObjectId}");
    response.EnsureSuccessStatusCode();
}

Testing REST API

Controller Integration Tests

Basic REST API Test:

[TestClass]
public class MicroserviceAggregateRootsControllerTests 
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> factory;

    public MicroserviceAggregateRootsControllerTests(
        WebApplicationFactory<Program> factory)
    {
        this.factory = factory;
    }

    [TestMethod]
    public async Task CreateMicroserviceAggregateRoot_Should_Return_Created()
    {
        // Arrange
        var client = this.factory.CreateClient();
        var request = new CreateMicroserviceAggregateRootRequest
        {
            ObjectId = Guid.NewGuid()
        };

        // Act
        var response = await client.PostAsJsonAsync(
            "/api/microserviceAggregateRoots", 
            request);

        // Assert
        Assert.AreEqual(HttpStatusCode.Created, response.StatusCode);
        var content = await response.Content.ReadFromJsonAsync<CreateMicroserviceAggregateRootResponse>();
        Assert.IsNotNull(content);
        Assert.AreEqual(request.ObjectId, content.ObjectId);
    }

    [TestMethod]
    public async Task GetMicroserviceAggregateRoot_Should_Return_Ok()
    {
        // Arrange
        var client = this.factory.CreateClient();
        var id = Guid.NewGuid();

        // Create entity first
        await client.PostAsJsonAsync("/api/microserviceAggregateRoots",
            new CreateMicroserviceAggregateRootRequest { ObjectId = id });

        // Act
        var response = await client.GetAsync($"/api/microserviceAggregateRoots/{id}");

        // Assert
        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadFromJsonAsync<MicroserviceAggregateRootResponse>();
        Assert.IsNotNull(content);
        Assert.AreEqual(id, content.ObjectId);
    }

    [TestMethod]
    public async Task DeleteMicroserviceAggregateRoot_Should_Return_NoContent()
    {
        // Arrange
        var client = this.factory.CreateClient();
        var id = Guid.NewGuid();

        // Create entity first
        await client.PostAsJsonAsync("/api/microserviceAggregateRoots",
            new CreateMicroserviceAggregateRootRequest { ObjectId = id });

        // Act
        var response = await client.DeleteAsync($"/api/microserviceAggregateRoots/{id}");

        // Assert
        Assert.AreEqual(HttpStatusCode.NoContent, response.StatusCode);

        // Verify deletion
        var getResponse = await client.GetAsync($"/api/microserviceAggregateRoots/{id}");
        Assert.AreEqual(HttpStatusCode.NotFound, getResponse.StatusCode);
    }
}

Testing Validation

Test Model Validation:

[TestMethod]
public async Task CreateMicroserviceAggregateRoot_InvalidInput_Should_Return_BadRequest()
{
    // Arrange
    var client = this.factory.CreateClient();
    var invalidRequest = new CreateMicroserviceAggregateRootRequest
    {
        ObjectId = Guid.Empty // Invalid: should not be empty
    };

    // Act
    var response = await client.PostAsJsonAsync(
        "/api/microserviceAggregateRoots", 
        invalidRequest);

    // Assert
    Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode);

    var problemDetails = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
    Assert.IsNotNull(problemDetails);
    Assert.IsTrue(problemDetails.Errors.ContainsKey("ObjectId"));
}

Testing Authentication and Authorization

Test Authenticated Endpoints:

[TestMethod]
public async Task GetProtectedResource_WithoutAuth_Should_Return_Unauthorized()
{
    // Arrange
    var client = this.factory.CreateClient();

    // Act
    var response = await client.GetAsync("/api/protected/resource");

    // Assert
    Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode);
}

[TestMethod]
public async Task GetProtectedResource_WithAuth_Should_Return_Ok()
{
    // Arrange
    var client = this.factory.CreateClient();
    var token = await GetTestTokenAsync();
    client.DefaultRequestHeaders.Authorization = 
        new AuthenticationHeaderValue("Bearer", token);

    // Act
    var response = await client.GetAsync("/api/protected/resource");

    // Assert
    response.EnsureSuccessStatusCode();
}

Testing gRPC Services

gRPC Integration Tests

Basic gRPC Test:

using Grpc.Net.Client;
using Microsoft.AspNetCore.TestHost;

[TestClass]
public class GrpcMicroserviceAggregateRootTests
{
    private TestServer? server;
    private GrpcChannel? channel;

    [TestInitialize]
    public void Setup()
    {
        var host = CreateHostBuilder().Build();
        host.Start();
        this.server = host.GetTestServer();

        var handler = this.server.CreateHandler();
        var client = new HttpClient(handler)
        {
            BaseAddress = new Uri("https://localhost")
        };

        this.channel = GrpcChannel.ForAddress(
            client.BaseAddress!,
            new GrpcChannelOptions { HttpClient = client });
    }

    [TestCleanup]
    public void Cleanup()
    {
        this.channel?.Dispose();
        this.server?.Dispose();
    }

    [TestMethod]
    public async Task CreateMicroserviceAggregateRoot_Should_Return_Response()
    {
        // Arrange
        var client = new MicroserviceAggregateRootProcessService.MicroserviceAggregateRootProcessServiceClient(
            this.channel!);
        var request = new CreateMicroserviceAggregateRootRequest
        {
            ObjectId = Guid.NewGuid().ToString()
        };

        // Act
        var response = await client.CreateMicroserviceAggregateRootAsync(request);

        // Assert
        Assert.IsNotNull(response);
        Assert.AreEqual(request.ObjectId, response.ObjectId);
    }
}

Testing GraphQL

GraphQL Integration Tests

Basic GraphQL Test:

[TestClass]
public class GraphQLTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> factory;

    public GraphQLTests(WebApplicationFactory<Program> factory)
    {
        this.factory = factory;
    }

    [TestMethod]
    public async Task GraphQL_Query_Should_Return_Data()
    {
        // Arrange
        var client = this.factory.CreateClient();
        var query = @"
            query {
                microserviceAggregateRoot(objectId: ""123e4567-e89b-12d3-a456-426614174000"") {
                    objectId
                    name
                }
            }";

        var requestBody = new
        {
            query = query
        };

        // Act
        var response = await client.PostAsJsonAsync("/graphql", requestBody);

        // Assert
        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadAsStringAsync();
        var result = JsonSerializer.Deserialize<GraphQLResponse>(content);

        Assert.IsNotNull(result);
        Assert.IsNull(result.Errors);
        Assert.IsNotNull(result.Data);
    }

    [TestMethod]
    public async Task GraphQL_Mutation_Should_Create_Entity()
    {
        // Arrange
        var client = this.factory.CreateClient();
        var mutation = @"
            mutation {
                createMicroserviceAggregateRoot(input: { objectId: ""123e4567-e89b-12d3-a456-426614174000"" }) {
                    objectId
                }
            }";

        var requestBody = new { query = mutation };

        // Act
        var response = await client.PostAsJsonAsync("/graphql", requestBody);

        // Assert
        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadAsStringAsync();
        var result = JsonSerializer.Deserialize<GraphQLResponse>(content);

        Assert.IsNotNull(result);
        Assert.IsNull(result.Errors);
    }
}

Testing SignalR

SignalR Integration Tests

Basic SignalR Test:

using Microsoft.AspNetCore.SignalR.Client;

[TestClass]
public class SignalRTests
{
    private TestServer? server;
    private HubConnection? connection;

    [TestInitialize]
    public async Task Setup()
    {
        var host = CreateHostBuilder().Build();
        host.Start();
        this.server = host.GetTestServer();

        var handler = this.server.CreateHandler();
        var httpClient = new HttpClient(handler)
        {
            BaseAddress = new Uri("https://localhost")
        };

        this.connection = new HubConnectionBuilder()
            .WithUrl("https://localhost/webChatHub", options =>
            {
                options.HttpMessageHandlerFactory = _ => handler;
                options.Transports = HttpTransportType.LongPolling;
            })
            .Build();

        await this.connection.StartAsync();
    }

    [TestCleanup]
    public async Task Cleanup()
    {
        if (this.connection != null)
        {
            await this.connection.DisposeAsync();
        }
        this.server?.Dispose();
    }

    [TestMethod]
    public async Task BroadcastMessage_Should_Receive_Message()
    {
        // Arrange
        string? receivedMessage = null;
        this.connection!.On<string>("NotifyMessageToAll", message =>
        {
            receivedMessage = message;
        });

        // Act
        await this.connection.InvokeAsync("BroadcastMessage", "Test Message");

        // Assert
        await Task.Delay(100); // Allow message propagation
        Assert.AreEqual("Test Message", receivedMessage);
    }
}

Database Testing

In-Memory Database Testing

NHibernate In-Memory Testing:

[TestClass]
public class RepositoryIntegrationTests
{
    private ISessionFactory? sessionFactory;
    private ISession? session;

    [TestInitialize]
    public void Setup()
    {
        // Configure in-memory SQLite database
        var configuration = new Configuration()
            .Configure()
            .SetProperty(NHibernate.Cfg.Environment.ConnectionString,
                "Data Source=:memory:;Version=3;New=True;")
            .SetProperty(NHibernate.Cfg.Environment.Dialect,
                typeof(SQLiteDialect).AssemblyQualifiedName);

        // Add mappings
        var modelMapper = new ModelMapper();
        modelMapper.AddMapping<MicroserviceAggregateRootEntityMap>();
        configuration.AddMapping(modelMapper.CompileMappingForAllExplicitlyAddedEntities());

        this.sessionFactory = configuration.BuildSessionFactory();
        this.session = this.sessionFactory.OpenSession();

        // Create schema
        var schemaExport = new SchemaExport(configuration);
        schemaExport.Execute(true, true, false, this.session.Connection, null);
    }

    [TestCleanup]
    public void Cleanup()
    {
        this.session?.Dispose();
        this.sessionFactory?.Dispose();
    }

    [TestMethod]
    public async Task Repository_Should_Save_And_Retrieve_Entity()
    {
        // Arrange
        var repository = new MicroserviceAggregateRootsRepository(this.session!);
        var entity = new MicroserviceAggregateRootEntity
        {
            ObjectId = Guid.NewGuid()
        };

        // Act
        await repository.InsertAsync(entity);
        await this.session!.FlushAsync();

        var retrieved = await repository.GetByIdAsync(entity.ObjectId);

        // Assert
        Assert.IsNotNull(retrieved);
        Assert.AreEqual(entity.ObjectId, retrieved.ObjectId);
    }
}

Test Containers

Using Testcontainers for Real Database Testing:

using Testcontainers.SqlServer;
using Testcontainers.MongoDb;

[TestClass]
public class DatabaseIntegrationTests : IAsyncLifetime
{
    private SqlServerContainer? sqlServerContainer;
    private MongoDbContainer? mongoDbContainer;

    public async Task InitializeAsync()
    {
        // Start SQL Server container
        this.sqlServerContainer = new SqlServerBuilder()
            .WithImage("mcr.microsoft.com/mssql/server:2022-latest")
            .WithPassword("Test123!")
            .Build();

        await this.sqlServerContainer.StartAsync();

        // Start MongoDB container
        this.mongoDbContainer = new MongoDbBuilder()
            .WithImage("mongo:7.0")
            .Build();

        await this.mongoDbContainer.StartAsync();
    }

    public async Task DisposeAsync()
    {
        await this.sqlServerContainer?.StopAsync()!;
        await this.mongoDbContainer?.StopAsync()!;
    }

    [TestMethod]
    public async Task Repository_WithRealDatabase_Should_Work()
    {
        // Arrange
        var connectionString = this.sqlServerContainer!.GetConnectionString();
        var repository = CreateRepository(connectionString);

        // Test with real database
        // ...
    }
}

Database Seeding

Seed Test Data:

public static class DatabaseSeeder
{
    public static async Task SeedTestDataAsync(
        IMicroserviceAggregateRootsRepository repository)
    {
        var entities = new[]
        {
            new MicroserviceAggregateRootEntity
            {
                ObjectId = Guid.Parse("11111111-1111-1111-1111-111111111111"),
                Name = "Test Entity 1"
            },
            new MicroserviceAggregateRootEntity
            {
                ObjectId = Guid.Parse("22222222-2222-2222-2222-222222222222"),
                Name = "Test Entity 2"
            }
        };

        foreach (var entity in entities)
        {
            await repository.InsertAsync(entity);
        }
    }
}

[TestClass]
public class ControllerTestsWithData : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> factory;

    [TestInitialize]
    public async Task Setup()
    {
        var client = this.factory.CreateClient();
        var server = this.factory.Server;

        // Access repository and seed data
        using var scope = server.Services.CreateScope();
        var repository = scope.ServiceProvider
            .GetRequiredService<IMicroserviceAggregateRootsRepository>();

        await DatabaseSeeder.SeedTestDataAsync(repository);
    }

    [TestMethod]
    public async Task Get_WithSeededData_Should_Return_Entities()
    {
        // Test with seeded data
    }
}

Service Mocking and Overrides

Mocking External Services

Replace External Service:

[TestMethod]
public async Task Test_WithMockedExternalService()
{
    var mockHttpClient = new Mock<HttpMessageHandler>();
    mockHttpClient
        .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(mockHttpClient.Object));
                return factory.Object;
            });
        });
    }).CreateClient();

    // Test with mocked HTTP client
}

Mocking Messaging

Replace Message Bus:

[TestMethod]
public async Task Test_WithMockedMessageBus()
{
    var mockEventBus = new Mock<IEventBus>();
    var publishedEvents = new List<object>();

    mockEventBus.Setup(b => b.PublishAsync(
            It.IsAny<object>(),
            It.IsAny<CancellationToken>()))
        .Callback<object, CancellationToken>((evt, ct) =>
        {
            publishedEvents.Add(evt);
        })
        .Returns(Task.CompletedTask);

    var client = this.factory.WithWebHostBuilder(builder =>
    {
        builder.ConfigureServices(services =>
        {
            services.RemoveAll<IEventBus>();
            services.AddSingleton(mockEventBus.Object);
        });
    }).CreateClient();

    // Create entity (should publish event)
    await client.PostAsJsonAsync("/api/microserviceAggregateRoots", request);

    // Assert event was published
    Assert.AreEqual(1, publishedEvents.Count);
    Assert.IsInstanceOfType(publishedEvents[0], typeof(MicroserviceAggregateRootCreatedEvent));
}

Configuration and Environment

Test Configuration

Test-Specific Configuration:

public class TestWebApplicationFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.UseEnvironment("Testing");

        builder.ConfigureAppConfiguration((context, config) =>
        {
            // Clear existing configuration
            config.Sources.Clear();

            // Add test configuration
            config.AddInMemoryCollection(new Dictionary<string, string>
            {
                ["Microservice:MicroserviceName"] = "TestService",
                ["Microservice:StartupWarmupSeconds"] = "0",
                ["Logging:LogLevel:Default"] = "Warning",
                ["ConnectionStrings:DefaultConnection"] = "Server=localhost;Database=TestDb;...",
                ["FeatureManagement:UseNewFeature"] = "true"
            });
        });
    }
}

Environment Variables

Set Environment Variables:

[TestMethod]
public async Task Test_WithEnvironmentVariables()
{
    Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Testing");
    Environment.SetEnvironmentVariable("TEST_CONFIG_VALUE", "test-value");

    try
    {
        var client = this.factory.CreateClient();
        // Test with environment variables
    }
    finally
    {
        Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", null);
        Environment.SetEnvironmentVariable("TEST_CONFIG_VALUE", null);
    }
}

Logging and Debugging

Capturing Logs

Capture Test Logs:

[TestMethod]
public async Task Test_WithLogCapture()
{
    var logMessages = new List<string>();

    var client = this.factory.WithWebHostBuilder(builder =>
    {
        builder.ConfigureLogging(logging =>
        {
            logging.ClearProviders();
            logging.AddProvider(new TestLoggerProvider(logMessages));
            logging.SetMinimumLevel(LogLevel.Debug);
        });
    }).CreateClient();

    // Execute test
    await client.GetAsync("/api/endpoint");

    // Assert logs
    Assert.IsTrue(logMessages.Any(m => m.Contains("expected message")));
}

public class TestLoggerProvider : ILoggerProvider
{
    private readonly List<string> logMessages;

    public TestLoggerProvider(List<string> logMessages)
    {
        this.logMessages = logMessages;
    }

    public ILogger CreateLogger(string categoryName) =>
        new TestLogger(categoryName, this.logMessages);

    public void Dispose() { }
}

Best Practices

Do's

  1. Use Test Fixtures for Shared Setup

    // ✅ GOOD - Shared factory across tests
    public class ControllerTests : IClassFixture<WebApplicationFactory<Program>>
    {
        private readonly WebApplicationFactory<Program> factory;
    
        public ControllerTests(WebApplicationFactory<Program> factory)
        {
            this.factory = factory;
        }
    }
    

  2. Isolate Tests with Fresh Data

    // ✅ GOOD - Each test has clean state
    [TestInitialize]
    public async Task Setup()
    {
        await this.ClearDatabaseAsync();
        await this.SeedTestDataAsync();
    }
    

  3. Test Real Scenarios

    // ✅ GOOD - Test complete user journey
    [TestMethod]
    public async Task Create_Then_Read_Then_Delete_Should_Work()
    {
        // Create
        var createResponse = await client.PostAsJsonAsync(...);
    
        // Read
        var readResponse = await client.GetAsync(...);
    
        // Delete
        var deleteResponse = await client.DeleteAsync(...);
    }
    

  4. Use Meaningful Assertions

    // ✅ GOOD - Specific assertions
    Assert.AreEqual(HttpStatusCode.Created, response.StatusCode);
    var content = await response.Content.ReadFromJsonAsync<Response>();
    Assert.AreEqual(expectedId, content.Id);
    

  5. Clean Up Resources

    // ✅ GOOD - Proper cleanup
    [TestCleanup]
    public void Cleanup()
    {
        this.client?.Dispose();
        this.server?.Dispose();
    }
    

Don'ts

  1. Don't Share State Between Tests

    // ❌ BAD - Shared state causes test interference
    private static List<Entity> sharedEntities = new();
    
    // ✅ GOOD - Each test has isolated state
    private List<Entity> testEntities = new();
    

  2. Don't Test Implementation Details

    // ❌ BAD - Testing internal implementation
    var processor = GetService<IMicroserviceAggregateRootsProcessor>();
    Assert.AreEqual(1, processor.InternalCounter);
    
    // ✅ GOOD - Testing public behavior
    var response = await client.PostAsJsonAsync(...);
    Assert.AreEqual(HttpStatusCode.Created, response.StatusCode);
    

  3. Don't Use Real External Services

    // ❌ BAD - Real external API calls
    var response = await client.GetAsync("https://external-api.com/data");
    
    // ✅ GOOD - Mock external services
    var mockHandler = new Mock<HttpMessageHandler>();
    // Configure mock
    

  4. Don't Ignore Async/Await

    // ❌ BAD - Missing await
    var response = client.GetAsync("/api/endpoint");
    
    // ✅ GOOD - Proper async/await
    var response = await client.GetAsync("/api/endpoint");
    

  5. Don't Hardcode Test Data

    // ❌ BAD - Hardcoded values
    var id = Guid.Parse("12345678-1234-1234-1234-123456789012");
    
    // ✅ GOOD - Generated test data
    var id = Guid.NewGuid();
    

Troubleshooting

Issue: TestServer Not Starting

Symptoms: TestServer fails to start or throws exceptions.

Solutions: 1. Check StartupWarmupSeconds is set to 0 in test configuration 2. Verify all required services are registered 3. Check for missing configuration values 4. Ensure database connection strings are valid (or use in-memory)

Issue: Database State Persists Between Tests

Symptoms: Tests fail when run together but pass individually.

Solutions: 1. Clear database in [TestInitialize] or [TestCleanup] 2. Use unique database names per test 3. Use transactions that rollback after each test 4. Use in-memory databases for faster tests

Issue: HttpClient Timeout

Symptoms: Tests timeout when making HTTP requests.

Solutions: 1. Increase HttpClient.Timeout:

var client = factory.CreateClient();
client.Timeout = TimeSpan.FromSeconds(30);
2. Check for deadlocks in async code 3. Verify services are not blocking

Issue: Services Not Found

Symptoms: GetRequiredService<T>() throws exception.

Solutions: 1. Verify service is registered in Startup or TestStartup 2. Check service lifetime (singleton vs scoped) 3. Ensure proper scope is used:

using var scope = server.Services.CreateScope();
var service = scope.ServiceProvider.GetRequiredService<T>();

Summary

Integration Testing in the ConnectSoft Microservice Template provides:

  • End-to-End Validation: Tests complete application stack
  • WebApplicationFactory: Full application testing with HTTP client
  • TestServer: In-memory testing with direct service access
  • Database Testing: Support for in-memory and test containers
  • Service Mocking: Replace external dependencies for testing
  • Configuration Overrides: Test-specific configuration
  • API Testing: REST, gRPC, GraphQL, and SignalR support
  • Authentication Testing: Test protected endpoints
  • Logging: Capture and assert on log messages
  • Isolation: Each test runs in clean state

By following these patterns, teams can:

  • Validate Integration: Ensure components work together correctly
  • Catch Regressions: Detect breaking changes early
  • Test Real Scenarios: Validate complete user journeys
  • Maintain Confidence: Fast feedback on integration issues
  • Support CI/CD: Run integration tests in pipelines
  • Document Behavior: Tests serve as living documentation

Integration tests complement unit tests by validating that the application works correctly as an integrated system, providing confidence that the microservice is ready for deployment.