DTDE Development Plan - Testing Strategy¶
← Back to Configuration & API | Next: Implementation Phases →
1. Testing Philosophy¶
Following DDD and .NET best practices from the instructions:
- Test Naming Convention:
MethodName_Condition_ExpectedResult() - Minimum Coverage: 85% for Domain and Application layers
- Test Categories: Unit, Integration, Acceptance, Performance
2. Test Project Structure¶
tests/
├── Dtde.Core.Tests/ # Domain layer unit tests
│ ├── Metadata/
│ │ ├── EntityMetadataTests.cs
│ │ ├── ValidityConfigurationTests.cs
│ │ ├── ShardMetadataTests.cs
│ │ └── MetadataRegistryTests.cs
│ ├── Sharding/
│ │ ├── DateRangeShardingStrategyTests.cs
│ │ ├── HashShardingStrategyTests.cs
│ │ └── CompositeShardingStrategyTests.cs
│ └── Temporal/
│ └── TemporalContextTests.cs
│
├── Dtde.EntityFramework.Tests/ # EF Core integration unit tests
│ ├── Configuration/
│ │ ├── DtdeOptionsBuilderTests.cs
│ │ └── FluentApiExtensionTests.cs
│ ├── Query/
│ │ ├── ExpressionRewriterTests.cs
│ │ ├── ShardQueryPlannerTests.cs
│ │ ├── QueryExecutorTests.cs
│ │ └── ResultMergerTests.cs
│ └── Update/
│ ├── UpdateProcessorTests.cs
│ ├── VersionManagerTests.cs
│ └── ShardWriteRouterTests.cs
│
├── Dtde.Integration.Tests/ # End-to-end integration tests
│ ├── Fixtures/
│ │ ├── TestDatabaseFixture.cs
│ │ ├── MultiShardFixture.cs
│ │ └── TestEntities.cs
│ ├── Query/
│ │ ├── TemporalQueryIntegrationTests.cs
│ │ ├── ShardedQueryIntegrationTests.cs
│ │ └── PaginationIntegrationTests.cs
│ ├── Update/
│ │ ├── VersionBumpIntegrationTests.cs
│ │ ├── CrossShardUpdateIntegrationTests.cs
│ │ └── ConcurrencyIntegrationTests.cs
│ └── Scenarios/
│ ├── ContractManagementScenarioTests.cs
│ └── HighVolumeScenarioTests.cs
│
└── Dtde.Benchmarks/ # Performance benchmarks
├── QueryBenchmarks.cs
├── UpdateBenchmarks.cs
└── ShardResolutionBenchmarks.cs
3. Unit Test Specifications¶
3.1 Metadata Tests¶
namespace Dtde.Core.Tests.Metadata;
public class ValidityConfigurationTests
{
[Fact(DisplayName = "ValidityConfiguration with both properties creates correct predicate")]
public void BuildPredicate_WithBothProperties_CreatesCorrectPredicate()
{
// Arrange
var validFrom = CreatePropertyMetadata<TestEntity>("EffectiveDate");
var validTo = CreatePropertyMetadata<TestEntity>("ExpirationDate");
var config = new ValidityConfiguration(validFrom, validTo);
var targetDate = new DateTime(2024, 6, 15);
// Act
var predicate = config.BuildPredicate<TestEntity>(targetDate);
var compiled = predicate.Compile();
// Assert
var validEntity = new TestEntity
{
EffectiveDate = new DateTime(2024, 1, 1),
ExpirationDate = new DateTime(2024, 12, 31)
};
var invalidEntity = new TestEntity
{
EffectiveDate = new DateTime(2025, 1, 1),
ExpirationDate = new DateTime(2025, 12, 31)
};
compiled(validEntity).Should().BeTrue();
compiled(invalidEntity).Should().BeFalse();
}
[Fact(DisplayName = "ValidityConfiguration with only start property allows open-ended validity")]
public void Constructor_WithOnlyStartProperty_AllowsOpenEndedValidity()
{
// Arrange
var validFrom = CreatePropertyMetadata<TestEntity>("EffectiveDate");
// Act
var config = new ValidityConfiguration(validFrom, validToProperty: null);
// Assert
config.ValidFromProperty.Should().NotBeNull();
config.ValidToProperty.Should().BeNull();
config.IsOpenEnded.Should().BeTrue();
}
[Fact(DisplayName = "ValidityConfiguration BuildPredicate handles null end date correctly")]
public void BuildPredicate_WithOpenEnded_HandlesNullEndDate()
{
// Arrange
var validFrom = CreatePropertyMetadata<TestEntity>("EffectiveDate");
var config = new ValidityConfiguration(validFrom);
var targetDate = new DateTime(2024, 6, 15);
// Act
var predicate = config.BuildPredicate<TestEntity>(targetDate);
var compiled = predicate.Compile();
// Assert
var entity = new TestEntity { EffectiveDate = new DateTime(2024, 1, 1) };
compiled(entity).Should().BeTrue();
}
}
public class MetadataRegistryTests
{
[Fact(DisplayName = "MetadataRegistry GetEntityMetadata returns configured entity")]
public void GetEntityMetadata_ConfiguredEntity_ReturnsMetadata()
{
// Arrange
var registry = CreateRegistryWithEntity<TestEntity>();
// Act
var metadata = registry.GetEntityMetadata<TestEntity>();
// Assert
metadata.Should().NotBeNull();
metadata!.ClrType.Should().Be(typeof(TestEntity));
}
[Fact(DisplayName = "MetadataRegistry GetEntityMetadata returns null for unconfigured entity")]
public void GetEntityMetadata_UnconfiguredEntity_ReturnsNull()
{
// Arrange
var registry = CreateEmptyRegistry();
// Act
var metadata = registry.GetEntityMetadata<UnconfiguredEntity>();
// Assert
metadata.Should().BeNull();
}
[Fact(DisplayName = "MetadataRegistry Validate fails for missing primary key")]
public void Validate_MissingPrimaryKey_ReturnsError()
{
// Arrange
var registry = CreateRegistryWithInvalidEntity();
// Act
var result = registry.Validate();
// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.Contains("primary key"));
}
[Fact(DisplayName = "MetadataRegistry Validate fails for overlapping shard ranges")]
public void Validate_OverlappingShardRanges_ReturnsError()
{
// Arrange
var registry = CreateRegistryWithOverlappingShards();
// Act
var result = registry.Validate();
// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.Contains("overlapping"));
}
}
3.2 Sharding Strategy Tests¶
namespace Dtde.Core.Tests.Sharding;
public class DateRangeShardingStrategyTests
{
[Fact(DisplayName = "DateRangeStrategy with temporal context returns intersecting shards")]
public void ResolveShards_WithTemporalContext_ReturnsIntersectingShards()
{
// Arrange
var strategy = new DateRangeShardingStrategy();
var registry = CreateShardRegistryWithQuarterlyShards();
var entity = CreateEntityMetadataWithDateSharding();
var predicates = new Dictionary<string, object?>();
var targetDate = new DateTime(2024, 3, 15); // Q1 2024
// Act
var shards = strategy.ResolveShards(entity, registry, predicates, targetDate);
// Assert
shards.Should().HaveCount(1);
shards[0].ShardId.Should().Be("Shard2024Q1");
}
[Fact(DisplayName = "DateRangeStrategy without temporal context returns all shards")]
public void ResolveShards_WithoutTemporalContext_ReturnsAllShards()
{
// Arrange
var strategy = new DateRangeShardingStrategy();
var registry = CreateShardRegistryWithQuarterlyShards();
var entity = CreateEntityMetadataWithDateSharding();
var predicates = new Dictionary<string, object?>();
// Act
var shards = strategy.ResolveShards(entity, registry, predicates, temporalContext: null);
// Assert
shards.Should().HaveCount(4); // All quarterly shards
}
[Fact(DisplayName = "DateRangeStrategy write operation returns correct shard")]
public void ResolveWriteShard_WithEntity_ReturnsCorrectShard()
{
// Arrange
var strategy = new DateRangeShardingStrategy();
var registry = CreateShardRegistryWithQuarterlyShards();
var entity = CreateEntityMetadataWithDateSharding();
var instance = new TestEntity { EffectiveDate = new DateTime(2024, 5, 1) }; // Q2 2024
// Act
var shard = strategy.ResolveWriteShard(entity, registry, instance);
// Assert
shard.ShardId.Should().Be("Shard2024Q2");
}
}
public class HashShardingStrategyTests
{
[Fact(DisplayName = "HashStrategy with key predicate returns single shard")]
public void ResolveShards_WithKeyPredicate_ReturnsSingleShard()
{
// Arrange
var strategy = new HashShardingStrategy(numberOfShards: 4);
var registry = CreateHashedShardRegistry(4);
var entity = CreateEntityMetadataWithHashSharding();
var predicates = new Dictionary<string, object?> { ["CustomerId"] = 12345 };
// Act
var shards = strategy.ResolveShards(entity, registry, predicates, temporalContext: null);
// Assert
shards.Should().HaveCount(1);
}
[Fact(DisplayName = "HashStrategy without key predicate returns all shards")]
public void ResolveShards_WithoutKeyPredicate_ReturnsAllShards()
{
// Arrange
var strategy = new HashShardingStrategy(numberOfShards: 4);
var registry = CreateHashedShardRegistry(4);
var entity = CreateEntityMetadataWithHashSharding();
var predicates = new Dictionary<string, object?>();
// Act
var shards = strategy.ResolveShards(entity, registry, predicates, temporalContext: null);
// Assert
shards.Should().HaveCount(4);
}
}
3.3 Expression Rewriter Tests¶
namespace Dtde.EntityFramework.Tests.Query;
public class DtdeExpressionRewriterTests
{
[Fact(DisplayName = "Rewrite with ValidAt injects temporal predicate")]
public void Rewrite_WithValidAt_InjectsTemporalPredicate()
{
// Arrange
var rewriter = CreateRewriter();
var query = CreateTestQuery().ValidAt(new DateTime(2024, 6, 15));
var temporalContext = CreateEmptyTemporalContext();
// Act
var result = rewriter.Rewrite(query.Expression, temporalContext);
// Assert
result.TemporalFiltersApplied.Should().BeTrue();
result.QueryDefinition.EffectiveTemporalPoint.Should().Be(new DateTime(2024, 6, 15));
}
[Fact(DisplayName = "Rewrite with WithVersions skips temporal filter")]
public void Rewrite_WithWithVersions_SkipsTemporalFilter()
{
// Arrange
var rewriter = CreateRewriter();
var query = CreateTestQuery().WithVersions();
var temporalContext = CreateTemporalContext(new DateTime(2024, 6, 15));
// Act
var result = rewriter.Rewrite(query.Expression, temporalContext);
// Assert
result.TemporalFiltersApplied.Should().BeFalse();
result.QueryDefinition.IncludeHistory.Should().BeTrue();
}
[Fact(DisplayName = "Rewrite with context temporal point uses context value")]
public void Rewrite_WithContextTemporalPoint_UsesContextValue()
{
// Arrange
var rewriter = CreateRewriter();
var query = CreateTestQuery(); // No ValidAt
var temporalContext = CreateTemporalContext(new DateTime(2024, 6, 15));
// Act
var result = rewriter.Rewrite(query.Expression, temporalContext);
// Assert
result.TemporalFiltersApplied.Should().BeTrue();
result.QueryDefinition.EffectiveTemporalPoint.Should().Be(new DateTime(2024, 6, 15));
}
[Fact(DisplayName = "Rewrite with query temporal point overrides context")]
public void Rewrite_WithQueryTemporalPoint_OverridesContext()
{
// Arrange
var rewriter = CreateRewriter();
var query = CreateTestQuery().ValidAt(new DateTime(2024, 1, 1));
var temporalContext = CreateTemporalContext(new DateTime(2024, 6, 15));
// Act
var result = rewriter.Rewrite(query.Expression, temporalContext);
// Assert
result.QueryDefinition.EffectiveTemporalPoint.Should().Be(new DateTime(2024, 1, 1));
}
[Fact(DisplayName = "Rewrite for non-temporal entity does not inject predicate")]
public void Rewrite_NonTemporalEntity_NoPredicateInjected()
{
// Arrange
var rewriter = CreateRewriter();
var query = CreateNonTemporalQuery();
var temporalContext = CreateTemporalContext(new DateTime(2024, 6, 15));
// Act
var result = rewriter.Rewrite(query.Expression, temporalContext);
// Assert
result.TemporalFiltersApplied.Should().BeFalse();
}
[Fact(DisplayName = "Rewrite with custom property names uses configured names")]
public void Rewrite_CustomPropertyNames_UsesConfiguredNames()
{
// Arrange
var rewriter = CreateRewriterWithCustomProperties();
var query = CreateQueryWithCustomEntity();
var temporalContext = CreateTemporalContext(new DateTime(2024, 6, 15));
// Act
var result = rewriter.Rewrite(query.Expression, temporalContext);
// Assert
// Verify the expression uses "StartDate" and "EndDate" properties
var expressionString = result.RewrittenExpression.ToString();
expressionString.Should().Contain("StartDate");
expressionString.Should().Contain("EndDate");
}
}
3.4 Update Processor Tests¶
namespace Dtde.EntityFramework.Tests.Update;
public class DtdeUpdateProcessorTests
{
[Fact(DisplayName = "ProcessUpdatesAsync for Added entity creates insert command")]
public void ProcessUpdatesAsync_AddedEntity_CreatesInsertCommand()
{
// Arrange
var processor = CreateProcessor();
var entry = CreateEntityEntry(EntityState.Added);
// Act
var result = await processor.ProcessUpdatesAsync(
CreateMockContext(),
new[] { entry },
CancellationToken.None);
// Assert
result.Should().Be(1);
// Verify insert command was created
}
[Fact(DisplayName = "ProcessUpdatesAsync for Modified entity creates version bump commands")]
public void ProcessUpdatesAsync_ModifiedEntity_CreatesVersionBumpCommands()
{
// Arrange
var processor = CreateProcessor();
var entry = CreateEntityEntry(EntityState.Modified);
// Act
var result = await processor.ProcessUpdatesAsync(
CreateMockContext(),
new[] { entry },
CancellationToken.None);
// Assert
result.Should().Be(2); // Invalidate old + Insert new
}
[Fact(DisplayName = "ProcessUpdatesAsync for Deleted entity creates close command")]
public void ProcessUpdatesAsync_DeletedEntity_CreatesCloseCommand()
{
// Arrange
var processor = CreateProcessor();
var entry = CreateEntityEntry(EntityState.Deleted);
// Act
var result = await processor.ProcessUpdatesAsync(
CreateMockContext(),
new[] { entry },
CancellationToken.None);
// Assert
result.Should().Be(1);
// Verify invalidate (close) command was created
}
}
4. Integration Test Specifications¶
4.1 Test Database Fixture¶
namespace Dtde.Integration.Tests.Fixtures;
public class MultiShardFixture : IAsyncLifetime
{
private readonly List<SqlConnection> _connections = new();
public string Shard1ConnectionString { get; private set; } = null!;
public string Shard2ConnectionString { get; private set; } = null!;
public string Shard3ConnectionString { get; private set; } = null!;
public async Task InitializeAsync()
{
// Create test databases for each shard
Shard1ConnectionString = await CreateTestDatabaseAsync("DtdeTest_Shard1");
Shard2ConnectionString = await CreateTestDatabaseAsync("DtdeTest_Shard2");
Shard3ConnectionString = await CreateTestDatabaseAsync("DtdeTest_Shard3");
// Apply migrations to each shard
await ApplyMigrationsAsync(Shard1ConnectionString);
await ApplyMigrationsAsync(Shard2ConnectionString);
await ApplyMigrationsAsync(Shard3ConnectionString);
}
public async Task DisposeAsync()
{
foreach (var connection in _connections)
{
await connection.DisposeAsync();
}
// Drop test databases
await DropTestDatabaseAsync("DtdeTest_Shard1");
await DropTestDatabaseAsync("DtdeTest_Shard2");
await DropTestDatabaseAsync("DtdeTest_Shard3");
}
public TestDbContext CreateContext()
{
var options = new DbContextOptionsBuilder<TestDbContext>()
.UseSqlServer(Shard1ConnectionString) // Default
.UseDtde(dtde =>
{
dtde.AddShard(s => s
.WithId("Shard1")
.WithConnectionString(Shard1ConnectionString)
.WithDateRange(new DateTime(2023, 1, 1), new DateTime(2024, 1, 1)));
dtde.AddShard(s => s
.WithId("Shard2")
.WithConnectionString(Shard2ConnectionString)
.WithDateRange(new DateTime(2024, 1, 1), new DateTime(2025, 1, 1)));
dtde.AddShard(s => s
.WithId("Shard3")
.WithConnectionString(Shard3ConnectionString)
.WithDateRange(new DateTime(2025, 1, 1), new DateTime(2100, 1, 1)));
})
.Options;
return new TestDbContext(options);
}
}
4.2 Temporal Query Integration Tests¶
namespace Dtde.Integration.Tests.Query;
public class TemporalQueryIntegrationTests : IClassFixture<MultiShardFixture>
{
private readonly MultiShardFixture _fixture;
public TemporalQueryIntegrationTests(MultiShardFixture fixture)
{
_fixture = fixture;
}
[Fact(DisplayName = "ValidAt query returns only valid entities")]
public async Task ValidAt_Query_ReturnsOnlyValidEntities()
{
// Arrange
await using var context = _fixture.CreateContext();
await SeedTestDataAsync(context);
var targetDate = new DateTime(2024, 6, 15);
// Act
var results = await context.Contracts
.ValidAt(targetDate)
.ToListAsync();
// Assert
results.Should().AllSatisfy(c =>
{
c.EffectiveDate.Should().BeLessThanOrEqualTo(targetDate);
c.ExpirationDate.Should().BeGreaterThan(targetDate);
});
}
[Fact(DisplayName = "ValidAt query resolves correct shards")]
public async Task ValidAt_Query_ResolvesCorrectShards()
{
// Arrange
await using var context = _fixture.CreateContext();
await SeedTestDataAsync(context);
var targetDate = new DateTime(2024, 6, 15);
// Act & Assert (verify through diagnostics)
var diagnosticEvents = new List<ShardResolvedEvent>();
context.SubscribeToDiagnostics(e => diagnosticEvents.Add(e));
await context.Contracts.ValidAt(targetDate).ToListAsync();
diagnosticEvents.Should().HaveCount(1);
diagnosticEvents[0].ShardIds.Should().Contain("Shard2");
}
[Fact(DisplayName = "WithVersions returns all versions")]
public async Task WithVersions_ReturnsAllVersions()
{
// Arrange
await using var context = _fixture.CreateContext();
var contractId = await SeedVersionedContractAsync(context, versionCount: 3);
// Act
var versions = await context.Contracts
.WithVersions()
.Where(c => c.Id == contractId)
.OrderBy(c => c.EffectiveDate)
.ToListAsync();
// Assert
versions.Should().HaveCount(3);
}
}
4.3 Sharded Query Integration Tests¶
namespace Dtde.Integration.Tests.Query;
public class ShardedQueryIntegrationTests : IClassFixture<MultiShardFixture>
{
[Fact(DisplayName = "Query across multiple shards merges results")]
public async Task Query_AcrossMultipleShards_MergesResults()
{
// Arrange
await using var context = _fixture.CreateContext();
await SeedDataAcrossAllShardsAsync(context);
// Act - Query that spans all shards
var results = await context.Contracts
.WithVersions()
.ToListAsync();
// Assert
results.Should().HaveCountGreaterThan(0);
// Verify results from all shards are present
}
[Fact(DisplayName = "Query with pagination applies globally")]
public async Task Query_WithPagination_AppliesGlobally()
{
// Arrange
await using var context = _fixture.CreateContext();
await SeedManyContractsAsync(context, count: 100);
// Act
var page1 = await context.Contracts
.WithVersions()
.OrderBy(c => c.ContractNumber)
.Skip(0).Take(10)
.ToListAsync();
var page2 = await context.Contracts
.WithVersions()
.OrderBy(c => c.ContractNumber)
.Skip(10).Take(10)
.ToListAsync();
// Assert
page1.Should().HaveCount(10);
page2.Should().HaveCount(10);
page1.Should().NotIntersectWith(page2);
}
[Fact(DisplayName = "Query with ordering sorts globally")]
public async Task Query_WithOrdering_SortsGlobally()
{
// Arrange
await using var context = _fixture.CreateContext();
await SeedDataAcrossAllShardsAsync(context);
// Act
var results = await context.Contracts
.WithVersions()
.OrderBy(c => c.ContractNumber)
.ToListAsync();
// Assert
results.Should().BeInAscendingOrder(c => c.ContractNumber);
}
}
4.4 Version Bump Integration Tests¶
namespace Dtde.Integration.Tests.Update;
public class VersionBumpIntegrationTests : IClassFixture<MultiShardFixture>
{
[Fact(DisplayName = "Update creates new version and closes old")]
public async Task Update_CreatesNewVersion_ClosesOld()
{
// Arrange
await using var context = _fixture.CreateContext();
var contract = await SeedSingleContractAsync(context);
var originalEffectiveDate = contract.EffectiveDate;
// Act
contract.Amount = 999.99m;
await context.SaveChangesAsync();
// Assert
var versions = await context.Contracts
.WithVersions()
.Where(c => c.Id == contract.Id)
.OrderBy(c => c.EffectiveDate)
.ToListAsync();
versions.Should().HaveCount(2);
versions[0].ExpirationDate.Should().BeLessThan(DateTime.MaxValue);
versions[1].Amount.Should().Be(999.99m);
}
[Fact(DisplayName = "Delete closes validity period")]
public async Task Delete_ClosesValidityPeriod()
{
// Arrange
await using var context = _fixture.CreateContext();
var contract = await SeedSingleContractAsync(context);
// Act
context.Contracts.Remove(contract);
await context.SaveChangesAsync();
// Assert - Should not be valid today
var activeContract = await context.Contracts
.ValidAt(DateTime.Today)
.FirstOrDefaultAsync(c => c.Id == contract.Id);
activeContract.Should().BeNull();
// But should be in history
var historicalContract = await context.Contracts
.WithVersions()
.FirstOrDefaultAsync(c => c.Id == contract.Id);
historicalContract.Should().NotBeNull();
historicalContract!.ExpirationDate.Should().BeLessThanOrEqualTo(DateTime.UtcNow);
}
}
5. Performance Benchmarks¶
5.1 Benchmark Configuration¶
namespace Dtde.Benchmarks;
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net80)]
public class QueryBenchmarks
{
private MultiShardFixture _fixture = null!;
private TestDbContext _context = null!;
[GlobalSetup]
public async Task Setup()
{
_fixture = new MultiShardFixture();
await _fixture.InitializeAsync();
_context = _fixture.CreateContext();
// Seed 1M rows across shards
await SeedLargeDatasetAsync(_context, rowCount: 1_000_000);
}
[GlobalCleanup]
public async Task Cleanup()
{
await _context.DisposeAsync();
await _fixture.DisposeAsync();
}
[Benchmark(Description = "ValidAt query - single shard")]
public async Task<List<Contract>> ValidAt_SingleShard()
{
return await _context.Contracts
.ValidAt(new DateTime(2024, 6, 15))
.Take(100)
.ToListAsync();
}
[Benchmark(Description = "ValidAt query - multiple shards")]
public async Task<List<Contract>> ValidAt_MultipleShards()
{
return await _context.Contracts
.ValidBetween(new DateTime(2023, 1, 1), new DateTime(2025, 1, 1))
.Take(100)
.ToListAsync();
}
[Benchmark(Description = "WithVersions query - all shards")]
public async Task<List<Contract>> WithVersions_AllShards()
{
return await _context.Contracts
.WithVersions()
.Take(100)
.ToListAsync();
}
[Benchmark(Description = "Paginated query - page 10")]
public async Task<List<Contract>> Paginated_Page10()
{
return await _context.Contracts
.ValidAt(DateTime.Today)
.OrderBy(c => c.ContractNumber)
.Skip(90).Take(10)
.ToListAsync();
}
}
5.2 Performance Targets¶
| Benchmark | Target | Acceptable |
|---|---|---|
| Single shard ValidAt (100 rows) | < 50ms | < 100ms |
| Multi-shard ValidAt (100 rows) | < 100ms | < 200ms |
| WithVersions all shards (100 rows) | < 150ms | < 300ms |
| Paginated query page 10 | < 100ms | < 200ms |
| Version bump (single entity) | < 50ms | < 100ms |
| Bulk insert (1000 entities) | < 2s | < 5s |
6. Test Data Generators¶
namespace Dtde.Integration.Tests.Fixtures;
public static class TestDataGenerator
{
public static IEnumerable<Contract> GenerateContracts(
int count,
DateTime startDate,
DateTime endDate)
{
var random = new Random(42); // Deterministic for reproducibility
for (var i = 0; i < count; i++)
{
var effectiveDate = startDate.AddDays(random.Next((int)(endDate - startDate).TotalDays));
var duration = TimeSpan.FromDays(random.Next(30, 365));
yield return new Contract
{
ContractNumber = $"CONTRACT-{i:D8}",
Amount = (decimal)(random.NextDouble() * 10000),
CustomerName = $"Customer {i}",
EffectiveDate = effectiveDate,
ExpirationDate = effectiveDate + duration
};
}
}
public static async Task SeedLargeDatasetAsync(
TestDbContext context,
int rowCount)
{
var contracts = GenerateContracts(
rowCount,
new DateTime(2023, 1, 1),
new DateTime(2025, 12, 31));
foreach (var batch in contracts.Chunk(1000))
{
context.Contracts.AddRange(batch);
await context.SaveChangesAsync();
}
}
}
7. Continuous Integration¶
7.1 Test Pipeline¶
# .github/workflows/test.yml
name: Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- run: dotnet test tests/Dtde.Core.Tests --configuration Release
- run: dotnet test tests/Dtde.EntityFramework.Tests --configuration Release
integration-tests:
runs-on: ubuntu-latest
services:
sqlserver:
image: mcr.microsoft.com/mssql/server:2022-latest
env:
SA_PASSWORD: YourStrong@Passw0rd
ACCEPT_EULA: Y
ports:
- 1433:1433
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- run: dotnet test tests/Dtde.Integration.Tests --configuration Release
benchmarks:
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
- run: dotnet run --project tests/Dtde.Benchmarks --configuration Release
7.2 Coverage Requirements¶
<!-- Directory.Build.props -->
<PropertyGroup>
<CollectCoverage>true</CollectCoverage>
<CoverletOutputFormat>cobertura</CoverletOutputFormat>
<Threshold>85</Threshold>
<ThresholdType>line,branch</ThresholdType>
<ThresholdStat>total</ThresholdStat>
</PropertyGroup>
8. Quality Gates¶
| Gate | Threshold | Enforcement |
|---|---|---|
| Unit Test Coverage | ≥ 85% | CI pipeline failure |
| Integration Test Pass Rate | 100% | CI pipeline failure |
| Benchmark Regression | < 10% slower | PR review warning |
| Code Quality (SonarQube) | 0 critical/high issues | Merge blocked |
Next Steps¶
Continue to 08 - Implementation Phases for milestone planning and delivery timeline.