In enterprise software engineering, a notorious paradox frequently arises: unit test coverage exceeds 90%, yet production releases regularly encounter catastrophic runtime failures.
The root cause lies in the excessive reliance on “mock objects.” Unit tests relying on in-memory mocks bypass real database engines entirely, leaving critical physical facts untested:
- SQL dialect incompatibilities: Complex queries execute seamlessly in in-memory SQLite yet trigger full table scans or syntax errors in production SQL Server instances due to implicit type conversions;
- Leaky multi-tenant filters: Mocked repositories fail to assert whether global query filters (
WHERE TenantId = @TenantId) are actually evaluated by the query compiler; - Transaction and Outbox consistency: Mock tests cannot verify whether domain entity updates and CAP Transactional Outbox records commit atomically in the same local database transaction.
BitzOrcas.Modern mandates real containerized integration tests via Testcontainers as a deployment gate. By spinning up immutable, SHA-256 digest-pinned SQL Server 2022 and Redis containers during automated test execution, the suite verifies the full 10-tier pipeline, true atomic transactions, and 100% behavioral equivalence across dual ORM adapters.
Three-Dimensional Testing Pyramid
Step 1: Constructing Platform-Parity Test Fixtures
In teams collaborating across macOS (Apple Silicon) and Linux workstations, unconstrained container instantiation frequently triggers CPU architecture drift.
Under tests/BitzOrcas.Integration.Tests/Infrastructure/, implement a test fixture pinned by cryptographic SHA-256 digest and platform architecture:
using DotNet.Testcontainers.Images;using Testcontainers.MsSql;using Xunit;using ContainerPlatform = DotNet.Testcontainers.Images.Platform;
namespace BitzOrcas.Integration.Tests.Infrastructure;
/// <summary>/// Integration test SQL Server container factory./// </summary>internal static class SqlServerTestContainer{ /// <summary> /// Official immutable image digest for SQL Server 2022 CU. /// Declaring linux/amd64 explicitly prevents Apple Silicon workstations from misinterpreting architecture as image drift. /// </summary> private const string ImmutableImageDigest = "mcr.microsoft.com/mssql/server@sha256:e07b9699a2b749969f19d86563ceeea22bd3a69f7f1db85a8d1ac4bdaf0c6f56";
internal static MsSqlContainer Create() => new MsSqlBuilder(new DockerImage(ImmutableImageDigest, new ContainerPlatform("linux/amd64"))).Build();}
/// <summary>/// Shares a single SQL Server container lifecycle across a test class./// </summary>public sealed class SqlServerContainerFixture : IAsyncLifetime{ public MsSqlContainer Container { get; } = SqlServerTestContainer.Create();
public async Task InitializeAsync() { // Start container and execute internal readiness checks await Container.StartAsync(); }
public Task DisposeAsync() => Container.DisposeAsync().AsTask();}Step 2: Implement End-to-End Slice Integration Tests
Leverage ASP.NET Core’s WebApplicationFactory<Program> to inject container connection strings into the API host and issue real HTTP requests:
using System.Net;using System.Net.Http.Json;using BitzOrcas.Domain.Results;using BitzOrcas.Integration.Tests.Infrastructure;using BitzOrcas.Modules.Legal.Application.Commands.CreateMatterIntake;using Microsoft.AspNetCore.Hosting;using Microsoft.AspNetCore.Mvc.Testing;using Shouldly;using Xunit;
public sealed class MatterIntakeIntegrationTests : IClassFixture<SqlServerContainerFixture>{ private readonly SqlServerContainerFixture _fixture;
public MatterIntakeIntegrationTests(SqlServerContainerFixture fixture) { _fixture = fixture; }
[Fact] public async Task CreateMatter_TraversingFullPipeline_ShouldPersistToDatabaseAndCapOutbox() { // 1. Override host configuration with active container connection string var appFactory = new WebApplicationFactory<Program>().WithWebHostBuilder(builder => { builder.UseSetting("ConnectionStrings:Default", _fixture.Container.GetConnectionString()); });
var client = appFactory.CreateClient();
// 2. Construct realistic litigation intake command payload var command = new CreateMatterIntakeCommand( MatterTitle: "Cross-Border Intellectual Property Injunction", ClientName: "Leading Global Retail Corp.", OpposingParty: "Offshore Infringing Distribution Network", ClaimAmount: 8_800_000.00m, Category: MatterCategory.CrossBorder);
// 3. Dispatch real HTTP POST request var response = await client.PostAsJsonAsync("/api/legal/matters", command);
// 4. Machine assertions: assert HTTP 200 OK and valid matter ID response.StatusCode.ShouldBe(HttpStatusCode.OK); var result = await response.Content.ReadFromJsonAsync<Result<string>>(); result.ShouldNotBeNull(); result.IsSuccess.ShouldBeTrue(); var matterId = result.Value; matterId.ShouldNotBeNullOrWhiteSpace();
// 5. Query underlying physical database to assert persistence and tenancy using var connection = new Microsoft.Data.SqlClient.SqlConnection(_fixture.Container.GetConnectionString()); await connection.OpenAsync();
using var cmd = connection.CreateCommand(); cmd.CommandText = "SELECT COUNT(1) FROM LegalMatterIntake WHERE Id = @Id AND IsDeleted = 0"; cmd.Parameters.AddWithValue("@Id", matterId); var count = (int)(await cmd.ExecuteScalarAsync() ?? 0); count.ShouldBe(1); }}Step 3: Dual-ORM Behavioral Parity Testing
BitzOrcas.Modern supports SqlSugar and EF Core as co-equal production persistence adapters. To guarantee that switching adapters introduces zero behavioral regressions, we write automated parity tests:
using System;using System.Threading;using System.Threading.Tasks;using BitzOrcas.Domain.Abstractions;using BitzOrcas.Infrastructure.EfCore;using BitzOrcas.Infrastructure.SqlSugar;using BitzOrcas.Integration.Tests.Infrastructure;using BitzOrcas.Modules.Legal.Domain;using Microsoft.Extensions.DependencyInjection;using Shouldly;using Xunit;
public sealed class OrmParityTests : IClassFixture<SqlServerContainerFixture>{ private readonly SqlServerContainerFixture _fixture;
public OrmParityTests(SqlServerContainerFixture fixture) { _fixture = fixture; }
[Theory] [InlineData("SqlSugar")] [InlineData("EFCore")] public async Task BothOrmAdapters_MustEnforceSoftDeleteAndTenantFilterIdentically(string ormProvider) { // 1. Resolve command repository port for active provider var repository = ResolveRepositoryForProvider<MatterIntake>(ormProvider, _fixture.Container.GetConnectionString());
var matterId = Guid.NewGuid().ToString("N"); var matter = MatterIntake.Create( matterId, $"CODE-{matterId[..6]}", "Dual-ORM Parity Verification Case", "Client Alpha", "Opposing Beta", 100_000.00m, tenantId: "1000001").GetValueOrThrow();
// 2. Persist aggregate var saveResult = await repository.SaveAsync(matter, CancellationToken.None); saveResult.IsSuccess.ShouldBeTrue();
// 3. Perform soft deletion matter.Delete(deletedBy: "auditor_user"); await repository.SaveAsync(matter, CancellationToken.None);
// 4. Assert: standard tenant queries across both ORMs return NotFound var queried = await repository.FindAsync(matterId, CancellationToken.None); queried.IsFailure.ShouldBeTrue(); queried.Error.Code.ShouldBe("Repository.Aggregate.NotFound"); }
private static ICommandRepository<TEntity, string> ResolveRepositoryForProvider<TEntity>(string provider, string connectionString) where TEntity : class { var services = new ServiceCollection(); services.AddLogging();
if (string.Equals(provider, "SqlSugar", StringComparison.OrdinalIgnoreCase)) { services.AddBitzOrcasSqlSugar(options => { options.ConnectionString = connectionString; }); } else { services.AddBitzOrcasEfCore(options => { options.ConnectionString = connectionString; }); }
var serviceProvider = services.BuildServiceProvider(); return serviceProvider.GetRequiredService<ICommandRepository<TEntity, string>>(); }}Core Engineering Quality Benefits
- Zero Environmental Drift: SHA-256 digest-pinned images guarantee that local workstations and CI test runners execute identical container runtimes.
- Confidence in Multi-ORM Deployments: Parity tests ensure business teams can seamlessly migrate between SqlSugar and EF Core without touching business logic.
- True Transactional Assertions: Testcontainers verifies local database transactions, unique index constraints, and atomic CAP Outbox commits against real relational storage.