-
Notifications
You must be signed in to change notification settings - Fork 1
Integrate Testcontainers for zero-setup integration testing #37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Integrate Testcontainers for zero-setup integration testing #37
Conversation
Co-authored-by: veggerby <[email protected]>
…n tests Co-authored-by: veggerby <[email protected]>
…have pre-existing Marten issues) Co-authored-by: veggerby <[email protected]>
Co-authored-by: veggerby <[email protected]>
… testing Co-authored-by: veggerby <[email protected]>
736e26a to
670249c
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request integrates Testcontainers for .NET to eliminate manual Docker container setup for integration tests, enabling zero-configuration integration testing for Redis and PostgreSQL dependencies.
Changes:
- Added Testcontainers infrastructure with fixtures for Redis and PostgreSQL container lifecycle management
- Refactored existing integration tests to use Testcontainers instead of environment variables (POSTGRES_CONNECTION, SHARDIS_TEST_PG)
- Updated CI/CD workflows to remove GitHub Actions service containers and rely on Testcontainers
- Created comprehensive integration testing documentation
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| test/Shardis.Tests/RedisContainerFixture.cs | New fixture managing Redis 7-alpine container lifecycle for integration tests |
| test/Shardis.Tests/RedisShardMapStoreIntegrationTests.cs | New integration tests for RedisShardMapStore using Testcontainers |
| test/Shardis.Tests/Shardis.Tests.csproj | Added Testcontainers packages and Shardis.Redis project reference |
| test/Shardis.Marten.Tests/PostgresContainerFixture.cs | Refactored to use Testcontainers instead of POSTGRES_CONNECTION env var |
| test/Shardis.Marten.Tests/PostgresFactAttribute.cs | Updated to remove env var check (Testcontainers manages lifecycle) |
| test/Shardis.Marten.Tests/MartenQueryExecutorTests.cs | Updated to use PostgresContainerFixture instead of env var |
| test/Shardis.Marten.Tests/MartenMetricsAndCancellationTests.cs | Added integration test trait |
| test/Shardis.Marten.Tests/AdaptivePagingTelemetryTests.cs | Added integration test trait |
| test/Shardis.Marten.Tests/Shardis.Marten.Tests.csproj | Added Testcontainers packages |
| test/Shardis.Migration.Marten.Tests/PostgresContainerFixture.cs | New fixture for migration integration tests |
| test/Shardis.Migration.Marten.Tests/MartenExecutorIntegrationTests.cs | Refactored to use PostgresContainerFixture instead of SHARDIS_TEST_PG env var |
| test/Shardis.Migration.Marten.Tests/Shardis.Migration.Marten.Tests.csproj | Added Testcontainers packages |
| Directory.Packages.props | Added Testcontainers package versions (4.2.0) |
| .github/workflows/integration-tests.yml | New workflow for Redis and Marten integration tests using Testcontainers |
| .github/workflows/integration-marten.yml | Updated to use Testcontainers instead of GitHub Actions service containers |
| docs/testing/integration-tests.md | Comprehensive new guide for integration testing with Testcontainers |
| README.md | Updated testing section with integration test filtering and Testcontainers info |
| CONTRIBUTING.md | Updated local setup section with integration test guidance |
| .github/copilot-instructions.md | Updated integration testing section to reflect Testcontainers usage |
| - name: Setup .NET | ||
| uses: actions/setup-dotnet@v4 | ||
| with: | ||
| dotnet-version: '9.0.x' |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The .NET version is inconsistent across workflows. The integration-tests.yml workflow uses dotnet-version: '9.0.x' while the integration-marten.yml workflow uses dotnet-version: "10.0.x". For consistency and to align with the coding guidelines which mention .NET 9.0 as a requirement, both workflows should use the same .NET version.
| dotnet-version: '9.0.x' | |
| dotnet-version: "10.0.x" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 702b5fd - Updated to dotnet-version: '10.0.x' for consistency across workflows.
| [Fact] | ||
| public async Task AssignShardToKeyAsync_CanStoreAndRetrieveKey() | ||
| { | ||
| // arrange | ||
| var store = new RedisShardMapStore<string>(_fixture.ConnectionString); | ||
| var key = new ShardKey<string>("user-123"); | ||
| var shard = new ShardId("shard-1"); | ||
|
|
||
| // act | ||
| var map = await store.AssignShardToKeyAsync(key, shard); | ||
| var retrieved = await store.TryGetShardIdForKeyAsync(key); | ||
|
|
||
| // assert | ||
| map.ShardKey.Should().Be(key); | ||
| map.ShardId.Should().Be(shard); | ||
| retrieved.Should().NotBeNull(); | ||
| retrieved!.Value.Should().Be(shard); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task TryAssignShardToKeyAsync_ReturnsCreatedTrueForNewKey() | ||
| { | ||
| // arrange | ||
| var store = new RedisShardMapStore<string>(_fixture.ConnectionString); | ||
| var key = new ShardKey<string>("user-456"); | ||
| var shard = new ShardId("shard-2"); | ||
|
|
||
| // act | ||
| var (created, map) = await store.TryAssignShardToKeyAsync(key, shard); | ||
|
|
||
| // assert | ||
| created.Should().BeTrue(); | ||
| map.ShardKey.Should().Be(key); | ||
| map.ShardId.Should().Be(shard); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task TryAssignShardToKeyAsync_ReturnsCreatedFalseForExistingKey() | ||
| { | ||
| // arrange | ||
| var store = new RedisShardMapStore<string>(_fixture.ConnectionString); | ||
| var key = new ShardKey<string>("user-789"); | ||
| var shard1 = new ShardId("shard-1"); | ||
| var shard2 = new ShardId("shard-2"); | ||
|
|
||
| // act | ||
| await store.AssignShardToKeyAsync(key, shard1); | ||
| var (created, map) = await store.TryAssignShardToKeyAsync(key, shard2); | ||
|
|
||
| // assert | ||
| created.Should().BeFalse(); | ||
| map.ShardKey.Should().Be(key); | ||
| map.ShardId.Should().Be(shard1); // original shard preserved | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task TryGetOrAddAsync_CreatesNewAssignmentWhenKeyDoesNotExist() | ||
| { | ||
| // arrange | ||
| var store = new RedisShardMapStore<string>(_fixture.ConnectionString); | ||
| var key = new ShardKey<string>("user-new"); | ||
| var shard = new ShardId("shard-3"); | ||
|
|
||
| // act | ||
| var (created, map) = await store.TryGetOrAddAsync(key, () => shard); | ||
|
|
||
| // assert | ||
| created.Should().BeTrue(); | ||
| map.ShardKey.Should().Be(key); | ||
| map.ShardId.Should().Be(shard); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task TryGetOrAddAsync_ReturnsExistingAssignmentWhenKeyExists() | ||
| { | ||
| // arrange | ||
| var store = new RedisShardMapStore<string>(_fixture.ConnectionString); | ||
| var key = new ShardKey<string>("user-existing"); | ||
| var shard1 = new ShardId("shard-1"); | ||
| var shard2 = new ShardId("shard-2"); | ||
| await store.AssignShardToKeyAsync(key, shard1); | ||
|
|
||
| // act | ||
| var (created, map) = await store.TryGetOrAddAsync(key, () => shard2); | ||
|
|
||
| // assert | ||
| created.Should().BeFalse(); | ||
| map.ShardKey.Should().Be(key); | ||
| map.ShardId.Should().Be(shard1); // original shard preserved | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task TryGetShardIdForKeyAsync_ReturnsNullForNonExistentKey() | ||
| { | ||
| // arrange | ||
| var store = new RedisShardMapStore<string>(_fixture.ConnectionString); | ||
| var key = new ShardKey<string>("non-existent-key"); | ||
|
|
||
| // act | ||
| var result = await store.TryGetShardIdForKeyAsync(key); | ||
|
|
||
| // assert | ||
| result.Should().BeNull(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void TryGetShardIdForKey_SyncVersion_CanRetrieveKey() | ||
| { | ||
| // arrange | ||
| var store = new RedisShardMapStore<string>(_fixture.ConnectionString); | ||
| var key = new ShardKey<string>("user-sync"); | ||
| var shard = new ShardId("shard-4"); | ||
| store.AssignShardToKey(key, shard); | ||
|
|
||
| // act | ||
| var found = store.TryGetShardIdForKey(key, out var retrievedShard); | ||
|
|
||
| // assert | ||
| found.Should().BeTrue(); | ||
| retrievedShard.Should().Be(shard); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void TryGetShardIdForKey_ReturnsFalseForNonExistentKey() | ||
| { | ||
| // arrange | ||
| var store = new RedisShardMapStore<string>(_fixture.ConnectionString); | ||
| var key = new ShardKey<string>("non-existent-sync"); | ||
|
|
||
| // act | ||
| var found = store.TryGetShardIdForKey(key, out var retrievedShard); | ||
|
|
||
| // assert | ||
| found.Should().BeFalse(); | ||
| retrievedShard.Should().Be(default(ShardId)); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void TryAssignShardToKey_SyncVersion_ReturnsCreatedTrueForNewKey() | ||
| { | ||
| // arrange | ||
| var store = new RedisShardMapStore<string>(_fixture.ConnectionString); | ||
| var key = new ShardKey<string>("user-sync-new"); | ||
| var shard = new ShardId("shard-5"); | ||
|
|
||
| // act | ||
| var created = store.TryAssignShardToKey(key, shard, out var map); | ||
|
|
||
| // assert | ||
| created.Should().BeTrue(); | ||
| map.ShardKey.Should().Be(key); | ||
| map.ShardId.Should().Be(shard); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void TryGetOrAdd_SyncVersion_CreatesNewAssignmentWhenKeyDoesNotExist() | ||
| { | ||
| // arrange | ||
| var store = new RedisShardMapStore<string>(_fixture.ConnectionString); | ||
| var key = new ShardKey<string>("user-sync-add"); | ||
| var shard = new ShardId("shard-6"); | ||
|
|
||
| // act | ||
| var created = store.TryGetOrAdd(key, () => shard, out var map); | ||
|
|
||
| // assert | ||
| created.Should().BeTrue(); | ||
| map.ShardKey.Should().Be(key); | ||
| map.ShardId.Should().Be(shard); | ||
| } | ||
| } |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Redis integration tests share a single container instance and use static key names like "user-new" and "non-existent-key". According to the coding guidelines (section "Best Practices"), tests should "Use unique prefixes/GUIDs in test data to avoid interference". Consider using Guid.NewGuid() or unique prefixes per test to ensure tests are truly isolated and can run in any order without interference.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 702b5fd - All Redis test keys now use Guid.NewGuid() for unique identifiers (e.g., $"user-{Guid.NewGuid()}") to ensure test isolation and prevent interference.
| using Testcontainers.Redis; | ||
|
|
||
| namespace Shardis.Tests; | ||
|
|
||
| /// <summary> | ||
| /// xUnit fixture that manages a Redis container lifecycle for integration tests. | ||
| /// </summary> | ||
| public sealed class RedisContainerFixture : IAsyncLifetime |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The RedisContainerFixture is missing required using statements. The class implements IAsyncLifetime from xUnit and uses Task types, but does not have the necessary using directives. Add using Xunit; for IAsyncLifetime and using System.Threading.Tasks; for Task types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 702b5fd - Added using System.Threading.Tasks; and using Xunit; to RedisContainerFixture.
| using Shardis.Model; | ||
| using Shardis.Redis; | ||
|
|
||
| namespace Shardis.Tests; | ||
|
|
||
| /// <summary> | ||
| /// Integration tests for RedisShardMapStore using Testcontainers. | ||
| /// </summary> | ||
| [Trait("Category", "Integration")] | ||
| public sealed class RedisShardMapStoreIntegrationTests : IClassFixture<RedisContainerFixture> |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The RedisShardMapStoreIntegrationTests is missing required using statements. The class uses [Fact] and [Trait] attributes from xUnit, and .Should() assertions from AwesomeAssertions, but does not have the necessary using directives. Add using Xunit; and using AwesomeAssertions;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 702b5fd - Added using Xunit; and using AwesomeAssertions; to RedisShardMapStoreIntegrationTests.
| - name: Run adaptive paging smoke benchmark (single iteration) (Testcontainers manages PostgreSQL) | ||
| run: | | ||
| dotnet run -c Release --project benchmarks/Shardis.Benchmarks.csproj -- --filter *AdaptivePagingBenchmarks* --iterationCount 1 --warmupCount 0 --job short | ||
| dotnet run -c Release --project benchmarks/Shardis.Benchmarks.csproj -- --filter *AdaptivePagingAllocationsBenchmarks* --iterationCount 1 --warmupCount 0 --exporters json --job short |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The adaptive-bench-smoke job in the workflow will fail because the benchmarks still rely on the POSTGRES_CONNECTION environment variable, which is no longer being set. The AdaptivePagingBenchmarks and AdaptivePagingAllocationsBenchmarks files expect this environment variable but the workflow was updated to use Testcontainers without updating the benchmark code to match. Either update the benchmarks to use Testcontainers fixtures or set the POSTGRES_CONNECTION environment variable in the workflow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 702b5fd - Restored PostgreSQL service container and POSTGRES_CONNECTION environment variable for the adaptive-bench-smoke job. Benchmarks are not using Testcontainers and continue to rely on the environment variable as before.
|
@copilot address the issues and concerns in the PR review |
…se unique GUIDs in tests, restore POSTGRES_CONNECTION for benchmarks Co-authored-by: veggerby <[email protected]>
🎯 Integration Testing Infrastructure with Testcontainers - COMPLETE
Summary
Successfully implemented Testcontainers-based integration testing infrastructure across Shardis, eliminating all manual Docker setup and providing deterministic, isolated, reproducible test environments.
✅ Test Results
Recent Updates (PR Review Feedback)
Fixed Issues
Phase Implementation Status
Phase 1: Core Package (Redis Integration) ✅ COMPLETE
test/Shardis.TestsRedisContainerFixture.cswith automatic lifecycle managementRedisShardMapStore<TKey>[Trait("Category", "Integration")]Phase 2: Marten Package (PostgreSQL Integration) ✅ COMPLETE
test/Shardis.Marten.TestsPostgresContainerFixture.csto use Testcontainers (removed env var dependency)[Trait("Category", "Integration")]Phase 3: Migration Packages⚠️ PARTIAL
test/Shardis.Migration.Marten.TestsPostgresContainerFixture.csfor migration scenariosEF Core migration tests(skipped - minimal changes principle)Verify checkpoint persistence(blocked by pre-existing Marten runtime compilation issues unrelated to Testcontainers)Note: Migration.Marten.Tests have pre-existing Marten JasperFx.RuntimeCompiler errors that existed before this PR.
Phase 4: CI/CD Integration ✅ COMPLETE
.github/workflows/integration-tests.ymlfor Redis and Marten tests.github/workflows/integration-marten.ymlto use Testcontainers for Marten testsdocker/setup-buildx-action@v3Phase 5: Documentation & Cleanup ✅ COMPLETE
docs/testing/integration-tests.mdguideREADME.mdwith integration test section and filtering examplesCONTRIBUTING.mdwith test categories and Docker requirements.github/copilot-instructions.mdwith Testcontainers guidelines🚀 Developer Experience Improvements
Before (Manual Setup)
Pain Points: Port conflicts, forgotten containers, version drift, cleanup issues
After (Testcontainers) ✨
Zero manual setup. Zero environment variables. Zero configuration.
📊 Files Changed Summary
Total: 20 files
By Category:
Directory.Packages.props, project files)🎁 Key Benefits
Developer Experience
✅ Zero manual setup:
git clone→dotnet test✅ Faster onboarding: New contributors run tests immediately
✅ Consistent environments: Same PostgreSQL 15, Redis 7 for everyone
✅ No port conflicts: Testcontainers assigns random ports automatically
✅ Test isolation: Unique GUIDs prevent test interference
CI/CD
✅ Higher reliability: No dependency on pre-configured services
✅ Faster feedback: Integration tests on every PR
✅ Parallel-safe: Isolated containers per test class
✅ Automatic cleanup: No resource leaks
Quality
✅ Better coverage: Easy to write → more tests get written
✅ Deterministic: Same images = consistent results
✅ Isolated: Tests don't interfere with each other
✅ Catch bugs earlier: Integration issues found in development
🧪 Running Tests
Prerequisites: Docker must be installed and running. That's it.
📚 Documentation
docs/testing/integration-tests.mdMigration.Marten.Tests: 4 tests failing due to pre-existing Marten runtime compilation errors (JasperFx.RuntimeCompiler issues) that existed before this PR. These are unrelated to the Testcontainers implementation and should be addressed in a separate issue.
✨ Conclusion
This PR delivers production-ready integration testing infrastructure for Shardis:
Status: Ready for merge pending review.
Original prompt
This section details on the original issue you should resolve
<issue_title>Integration Testing Infrastructure with Testcontainers</issue_title>
<issue_description># Integration Testing Infrastructure with Testcontainers
Overview
Systematically improve integration testing across all Shardis packages by adopting Testcontainers for deterministic, isolated, and reproducible test environments. This will eliminate manual infrastructure setup, improve CI reliability, and enable comprehensive integration test coverage.
Epic Type: Testing & Quality Assurance
Estimated Effort: 3-4 weeks
Priority: Medium-High (critical for production confidence)
Problem Statement
Current integration testing has several gaps:
docker runcommands for PostgreSQL and RedisCurrent State
From copilot-instructions.md:
Pain Points:
Scope
In Scope
Core Package Integration Tests (
test/Shardis.Tests/)RedisShardMapStore<TKey>Marten Integration Tests (
test/Shardis.Marten.Tests/)Migration Package Tests (
test/Shardis.Migration.Marten.Tests/,test/Shardis.Migration.EntityFrameworkCore.Tests/)Query Package Tests (
test/Shardis.Query.Tests/)CI/CD Integration
Out of Scope (Future Work)
Technical Requirements
1. Testcontainers .NET Integration
Package:
Testcontainers(official .NET library)2. PostgreSQL Container Fixture (Marten)
File:
test/Shardis.Marten.Tests/PostgresContainerFixture.cs