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¶
-
Use Test Fixtures for Shared Setup
-
Isolate Tests with Fresh Data
-
Test Real Scenarios
-
Use Meaningful Assertions
-
Clean Up Resources
Don'ts¶
-
Don't Share State Between Tests
-
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); -
Don't Use Real External Services
-
Don't Ignore Async/Await
-
Don't Hardcode Test Data
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:
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.