From ebc03d77ddc9a55550465ff71300c3d756979500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Tue, 13 Jun 2023 22:35:29 +0200 Subject: [PATCH] Introduce a new IDatabaseContainer interface This is required in order to implement a generic database fixture that could look like this. ```csharp using System; using System.Data.Common; using System.Threading.Tasks; using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Containers; using Testcontainers.MsSql; using Testcontainers.MySql; using Testcontainers.PostgreSql; using Xunit; namespace SampleCode; public abstract class DatabaseFixture : IAsyncLifetime where TBuilder : IContainerBuilder, new() where TContainer : IContainer, IDatabaseContainer { private string _connectionString; private TContainer _container; protected abstract DbProviderFactory ProviderFactory { get; } public string ConnectionString => _connectionString ?? throw new InvalidOperationException($"{nameof(IAsyncLifetime.InitializeAsync)} must be called before accessing the connection string"); async Task IAsyncLifetime.InitializeAsync() { _container = new TBuilder().Build(); await _container.StartAsync(); _connectionString = _container.GetConnectionString(); using var connection = ProviderFactory.CreateConnection() ?? throw new InvalidOperationException($"ProviderFactory.CreateConnection() returned null ({ProviderFactory})"); connection.ConnectionString = _connectionString; await connection.OpenAsync(); await connection.CloseAsync(); } async Task IAsyncLifetime.DisposeAsync() { if (_container != null) { await _container.StopAsync(); } } } public class MsSqlDbFixture : DatabaseFixture { protected override DbProviderFactory ProviderFactory => Microsoft.Data.SqlClient.SqlClientFactory.Instance; } public class MySqlDbFixture : DatabaseFixture { protected override DbProviderFactory ProviderFactory => MySqlConnector.MySqlConnectorFactory.Instance; } public class PostgreSqlDbFixture : DatabaseFixture { protected override DbProviderFactory ProviderFactory => Npgsql.NpgsqlFactory.Instance; } public class PostgreSqlTest : IClassFixture { private readonly PostgreSqlDbFixture _dbFixture; public PostgreSqlTest(PostgreSqlDbFixture dbFixture) => _dbFixture = dbFixture; [Fact] public async Task TestOnPostgreSql() { await using var connection = new Npgsql.NpgsqlConnection(_dbFixture.ConnectionString); await using var command = connection.CreateCommand(); // ... } } ``` This will also be required to implement a new wait strategy for database where an ADO.NET provider is available. --- Testcontainers.sln | 7 ++++++ .../AzuriteContainer.cs | 2 +- .../CosmosDbContainer.cs | 2 +- .../CouchDbContainer.cs | 2 +- .../CouchbaseContainer.cs | 2 +- .../DynamoDbContainer.cs | 2 +- .../ElasticsearchContainer.cs | 2 +- .../EventStoreDbContainer.cs | 2 +- .../LocalStackContainer.cs | 2 +- .../MariaDbContainer.cs | 2 +- src/Testcontainers.Minio/MinioContainer.cs | 2 +- .../MongoDbContainer.cs | 2 +- src/Testcontainers.MsSql/MsSqlContainer.cs | 2 +- src/Testcontainers.MySql/MySqlContainer.cs | 2 +- src/Testcontainers.Neo4j/Neo4jContainer.cs | 2 +- src/Testcontainers.Oracle/OracleContainer.cs | 2 +- .../PostgreSqlContainer.cs | 2 +- .../RabbitMqContainer.cs | 2 +- .../RavenDbContainer.cs | 2 +- src/Testcontainers.Redis/RedisContainer.cs | 2 +- .../SqlEdgeContainer.cs | 2 +- .../WebDriverContainer.cs | 2 +- .../Containers/IDatabaseContainer.cs | 17 +++++++++++++ .../.editorconfig | 1 + .../DatabasesContainerTest.cs | 25 +++++++++++++++++++ .../Testcontainers.Databases.Tests.csproj | 17 +++++++++++++ .../Testcontainers.Databases.Tests/Usings.cs | 7 ++++++ 27 files changed, 95 insertions(+), 21 deletions(-) create mode 100644 src/Testcontainers/Containers/IDatabaseContainer.cs create mode 100644 tests/Testcontainers.Databases.Tests/.editorconfig create mode 100644 tests/Testcontainers.Databases.Tests/DatabasesContainerTest.cs create mode 100644 tests/Testcontainers.Databases.Tests/Testcontainers.Databases.Tests.csproj create mode 100644 tests/Testcontainers.Databases.Tests/Usings.cs diff --git a/Testcontainers.sln b/Testcontainers.sln index 4a415068d..3024e4ab6 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -137,6 +137,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Tests", "tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.WebDriver.Tests", "tests\Testcontainers.WebDriver.Tests\Testcontainers.WebDriver.Tests.csproj", "{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Databases.Tests", "tests\Testcontainers.Databases.Tests\Testcontainers.Databases.Tests.csproj", "{DA54916E-1128-4200-B6AE-9F5BF02D832D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -394,6 +396,10 @@ Global {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.Build.0 = Release|Any CPU + {DA54916E-1128-4200-B6AE-9F5BF02D832D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DA54916E-1128-4200-B6AE-9F5BF02D832D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DA54916E-1128-4200-B6AE-9F5BF02D832D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DA54916E-1128-4200-B6AE-9F5BF02D832D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {3F2E254F-C203-43FD-A078-DC3E2CBC0F9F} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -458,5 +464,6 @@ Global {1A1983E6-5297-435F-B467-E8E1F11277D6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {27CDB869-A150-4593-958F-6F26E5391E7C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} + {DA54916E-1128-4200-B6AE-9F5BF02D832D} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} EndGlobalSection EndGlobal diff --git a/src/Testcontainers.Azurite/AzuriteContainer.cs b/src/Testcontainers.Azurite/AzuriteContainer.cs index a0fed14eb..3dc8a05de 100644 --- a/src/Testcontainers.Azurite/AzuriteContainer.cs +++ b/src/Testcontainers.Azurite/AzuriteContainer.cs @@ -2,7 +2,7 @@ namespace Testcontainers.Azurite; /// [PublicAPI] -public sealed class AzuriteContainer : DockerContainer +public sealed class AzuriteContainer : DockerContainer, IDatabaseContainer { private const string AccountName = "devstoreaccount1"; diff --git a/src/Testcontainers.CosmosDb/CosmosDbContainer.cs b/src/Testcontainers.CosmosDb/CosmosDbContainer.cs index 3f04d5742..cdee18239 100644 --- a/src/Testcontainers.CosmosDb/CosmosDbContainer.cs +++ b/src/Testcontainers.CosmosDb/CosmosDbContainer.cs @@ -2,7 +2,7 @@ namespace Testcontainers.CosmosDb; /// [PublicAPI] -public sealed class CosmosDbContainer : DockerContainer +public sealed class CosmosDbContainer : DockerContainer, IDatabaseContainer { /// /// Initializes a new instance of the class. diff --git a/src/Testcontainers.CouchDb/CouchDbContainer.cs b/src/Testcontainers.CouchDb/CouchDbContainer.cs index 1edfaac56..e1609b981 100644 --- a/src/Testcontainers.CouchDb/CouchDbContainer.cs +++ b/src/Testcontainers.CouchDb/CouchDbContainer.cs @@ -2,7 +2,7 @@ namespace Testcontainers.CouchDb; /// [PublicAPI] -public sealed class CouchDbContainer : DockerContainer +public sealed class CouchDbContainer : DockerContainer, IDatabaseContainer { private readonly CouchDbConfiguration _configuration; diff --git a/src/Testcontainers.Couchbase/CouchbaseContainer.cs b/src/Testcontainers.Couchbase/CouchbaseContainer.cs index 9da54d89c..d85a66001 100644 --- a/src/Testcontainers.Couchbase/CouchbaseContainer.cs +++ b/src/Testcontainers.Couchbase/CouchbaseContainer.cs @@ -2,7 +2,7 @@ namespace Testcontainers.Couchbase; /// [PublicAPI] -public sealed class CouchbaseContainer : DockerContainer +public sealed class CouchbaseContainer : DockerContainer, IDatabaseContainer { private readonly CouchbaseConfiguration _configuration; diff --git a/src/Testcontainers.DynamoDb/DynamoDbContainer.cs b/src/Testcontainers.DynamoDb/DynamoDbContainer.cs index 88064d3ea..0efedecc4 100644 --- a/src/Testcontainers.DynamoDb/DynamoDbContainer.cs +++ b/src/Testcontainers.DynamoDb/DynamoDbContainer.cs @@ -2,7 +2,7 @@ namespace Testcontainers.DynamoDb; /// [PublicAPI] -public sealed class DynamoDbContainer : DockerContainer +public sealed class DynamoDbContainer : DockerContainer, IDatabaseContainer { /// /// Initializes a new instance of the class. diff --git a/src/Testcontainers.Elasticsearch/ElasticsearchContainer.cs b/src/Testcontainers.Elasticsearch/ElasticsearchContainer.cs index 8cc24f9cb..095ddaba9 100644 --- a/src/Testcontainers.Elasticsearch/ElasticsearchContainer.cs +++ b/src/Testcontainers.Elasticsearch/ElasticsearchContainer.cs @@ -2,7 +2,7 @@ namespace Testcontainers.Elasticsearch; /// [PublicAPI] -public sealed class ElasticsearchContainer : DockerContainer +public sealed class ElasticsearchContainer : DockerContainer, IDatabaseContainer { private readonly ElasticsearchConfiguration _configuration; diff --git a/src/Testcontainers.EventStoreDb/EventStoreDbContainer.cs b/src/Testcontainers.EventStoreDb/EventStoreDbContainer.cs index 5f6579155..9e1ac6bca 100644 --- a/src/Testcontainers.EventStoreDb/EventStoreDbContainer.cs +++ b/src/Testcontainers.EventStoreDb/EventStoreDbContainer.cs @@ -2,7 +2,7 @@ namespace Testcontainers.EventStoreDb; /// [PublicAPI] -public sealed class EventStoreDbContainer : DockerContainer +public sealed class EventStoreDbContainer : DockerContainer, IDatabaseContainer { /// /// Initializes a new instance of the class. diff --git a/src/Testcontainers.LocalStack/LocalStackContainer.cs b/src/Testcontainers.LocalStack/LocalStackContainer.cs index 107a613e3..d1fb53b39 100644 --- a/src/Testcontainers.LocalStack/LocalStackContainer.cs +++ b/src/Testcontainers.LocalStack/LocalStackContainer.cs @@ -2,7 +2,7 @@ namespace Testcontainers.LocalStack; /// [PublicAPI] -public sealed class LocalStackContainer : DockerContainer +public sealed class LocalStackContainer : DockerContainer, IDatabaseContainer { /// /// Initializes a new instance of the class. diff --git a/src/Testcontainers.MariaDb/MariaDbContainer.cs b/src/Testcontainers.MariaDb/MariaDbContainer.cs index 97fba03c9..08b603344 100644 --- a/src/Testcontainers.MariaDb/MariaDbContainer.cs +++ b/src/Testcontainers.MariaDb/MariaDbContainer.cs @@ -2,7 +2,7 @@ namespace Testcontainers.MariaDb; /// [PublicAPI] -public sealed class MariaDbContainer : DockerContainer +public sealed class MariaDbContainer : DockerContainer, IDatabaseContainer { private readonly MariaDbConfiguration _configuration; diff --git a/src/Testcontainers.Minio/MinioContainer.cs b/src/Testcontainers.Minio/MinioContainer.cs index 776b37675..1c9272775 100644 --- a/src/Testcontainers.Minio/MinioContainer.cs +++ b/src/Testcontainers.Minio/MinioContainer.cs @@ -2,7 +2,7 @@ namespace Testcontainers.Minio; /// [PublicAPI] -public sealed class MinioContainer : DockerContainer +public sealed class MinioContainer : DockerContainer, IDatabaseContainer { private readonly MinioConfiguration _configuration; diff --git a/src/Testcontainers.MongoDb/MongoDbContainer.cs b/src/Testcontainers.MongoDb/MongoDbContainer.cs index 88ad50832..da7ad7406 100644 --- a/src/Testcontainers.MongoDb/MongoDbContainer.cs +++ b/src/Testcontainers.MongoDb/MongoDbContainer.cs @@ -2,7 +2,7 @@ namespace Testcontainers.MongoDb; /// [PublicAPI] -public sealed class MongoDbContainer : DockerContainer +public sealed class MongoDbContainer : DockerContainer, IDatabaseContainer { private readonly MongoDbConfiguration _configuration; diff --git a/src/Testcontainers.MsSql/MsSqlContainer.cs b/src/Testcontainers.MsSql/MsSqlContainer.cs index 865f38e20..78c8f2c36 100644 --- a/src/Testcontainers.MsSql/MsSqlContainer.cs +++ b/src/Testcontainers.MsSql/MsSqlContainer.cs @@ -2,7 +2,7 @@ namespace Testcontainers.MsSql; /// [PublicAPI] -public sealed class MsSqlContainer : DockerContainer +public sealed class MsSqlContainer : DockerContainer, IDatabaseContainer { private readonly MsSqlConfiguration _configuration; diff --git a/src/Testcontainers.MySql/MySqlContainer.cs b/src/Testcontainers.MySql/MySqlContainer.cs index 59dc19c8b..5b55be35e 100644 --- a/src/Testcontainers.MySql/MySqlContainer.cs +++ b/src/Testcontainers.MySql/MySqlContainer.cs @@ -2,7 +2,7 @@ namespace Testcontainers.MySql; /// [PublicAPI] -public sealed class MySqlContainer : DockerContainer +public sealed class MySqlContainer : DockerContainer, IDatabaseContainer { private readonly MySqlConfiguration _configuration; diff --git a/src/Testcontainers.Neo4j/Neo4jContainer.cs b/src/Testcontainers.Neo4j/Neo4jContainer.cs index 267037220..4ca641ff4 100644 --- a/src/Testcontainers.Neo4j/Neo4jContainer.cs +++ b/src/Testcontainers.Neo4j/Neo4jContainer.cs @@ -2,7 +2,7 @@ namespace Testcontainers.Neo4j; /// [PublicAPI] -public sealed class Neo4jContainer : DockerContainer +public sealed class Neo4jContainer : DockerContainer, IDatabaseContainer { /// /// Initializes a new instance of the class. diff --git a/src/Testcontainers.Oracle/OracleContainer.cs b/src/Testcontainers.Oracle/OracleContainer.cs index fa8cdab92..e1b1ae4c1 100644 --- a/src/Testcontainers.Oracle/OracleContainer.cs +++ b/src/Testcontainers.Oracle/OracleContainer.cs @@ -2,7 +2,7 @@ namespace Testcontainers.Oracle; /// [PublicAPI] -public sealed class OracleContainer : DockerContainer +public sealed class OracleContainer : DockerContainer, IDatabaseContainer { private readonly OracleConfiguration _configuration; diff --git a/src/Testcontainers.PostgreSql/PostgreSqlContainer.cs b/src/Testcontainers.PostgreSql/PostgreSqlContainer.cs index 3ac48447c..c771e6f11 100644 --- a/src/Testcontainers.PostgreSql/PostgreSqlContainer.cs +++ b/src/Testcontainers.PostgreSql/PostgreSqlContainer.cs @@ -2,7 +2,7 @@ namespace Testcontainers.PostgreSql; /// [PublicAPI] -public sealed class PostgreSqlContainer : DockerContainer +public sealed class PostgreSqlContainer : DockerContainer, IDatabaseContainer { private readonly PostgreSqlConfiguration _configuration; diff --git a/src/Testcontainers.RabbitMq/RabbitMqContainer.cs b/src/Testcontainers.RabbitMq/RabbitMqContainer.cs index 5f0431099..f5a198dee 100644 --- a/src/Testcontainers.RabbitMq/RabbitMqContainer.cs +++ b/src/Testcontainers.RabbitMq/RabbitMqContainer.cs @@ -2,7 +2,7 @@ namespace Testcontainers.RabbitMq; /// [PublicAPI] -public sealed class RabbitMqContainer : DockerContainer +public sealed class RabbitMqContainer : DockerContainer, IDatabaseContainer { private readonly RabbitMqConfiguration _configuration; diff --git a/src/Testcontainers.RavenDb/RavenDbContainer.cs b/src/Testcontainers.RavenDb/RavenDbContainer.cs index 8252aeb38..cdabb235b 100644 --- a/src/Testcontainers.RavenDb/RavenDbContainer.cs +++ b/src/Testcontainers.RavenDb/RavenDbContainer.cs @@ -2,7 +2,7 @@ namespace Testcontainers.RavenDb; /// [PublicAPI] -public sealed class RavenDbContainer : DockerContainer +public sealed class RavenDbContainer : DockerContainer, IDatabaseContainer { /// /// Initializes a new instance of the class. diff --git a/src/Testcontainers.Redis/RedisContainer.cs b/src/Testcontainers.Redis/RedisContainer.cs index ec3691919..209d15324 100644 --- a/src/Testcontainers.Redis/RedisContainer.cs +++ b/src/Testcontainers.Redis/RedisContainer.cs @@ -2,7 +2,7 @@ namespace Testcontainers.Redis; /// [PublicAPI] -public sealed class RedisContainer : DockerContainer +public sealed class RedisContainer : DockerContainer, IDatabaseContainer { /// /// Initializes a new instance of the class. diff --git a/src/Testcontainers.SqlEdge/SqlEdgeContainer.cs b/src/Testcontainers.SqlEdge/SqlEdgeContainer.cs index ccbb3124b..17ff0f4f1 100644 --- a/src/Testcontainers.SqlEdge/SqlEdgeContainer.cs +++ b/src/Testcontainers.SqlEdge/SqlEdgeContainer.cs @@ -2,7 +2,7 @@ namespace Testcontainers.SqlEdge; /// [PublicAPI] -public sealed class SqlEdgeContainer : DockerContainer +public sealed class SqlEdgeContainer : DockerContainer, IDatabaseContainer { private readonly SqlEdgeConfiguration _configuration; diff --git a/src/Testcontainers.WebDriver/WebDriverContainer.cs b/src/Testcontainers.WebDriver/WebDriverContainer.cs index 3c90cbc4e..864b337f3 100644 --- a/src/Testcontainers.WebDriver/WebDriverContainer.cs +++ b/src/Testcontainers.WebDriver/WebDriverContainer.cs @@ -2,7 +2,7 @@ namespace Testcontainers.WebDriver; /// [PublicAPI] -public sealed class WebDriverContainer : DockerContainer +public sealed class WebDriverContainer : DockerContainer, IDatabaseContainer { private readonly WebDriverConfiguration _configuration; diff --git a/src/Testcontainers/Containers/IDatabaseContainer.cs b/src/Testcontainers/Containers/IDatabaseContainer.cs new file mode 100644 index 000000000..25326322f --- /dev/null +++ b/src/Testcontainers/Containers/IDatabaseContainer.cs @@ -0,0 +1,17 @@ +namespace DotNet.Testcontainers.Containers +{ + using JetBrains.Annotations; + + /// + /// A database container instance. + /// + [PublicAPI] + public interface IDatabaseContainer + { + /// + /// Gets the connection string for connecting to the database. + /// + /// The connection string for connecting to the database. + string GetConnectionString(); + } +} diff --git a/tests/Testcontainers.Databases.Tests/.editorconfig b/tests/Testcontainers.Databases.Tests/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/tests/Testcontainers.Databases.Tests/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/tests/Testcontainers.Databases.Tests/DatabasesContainerTest.cs b/tests/Testcontainers.Databases.Tests/DatabasesContainerTest.cs new file mode 100644 index 000000000..b341babd1 --- /dev/null +++ b/tests/Testcontainers.Databases.Tests/DatabasesContainerTest.cs @@ -0,0 +1,25 @@ +namespace Testcontainers.Databases; + +public sealed class DatabaseContainersTest +{ + [Theory] + [MemberData(nameof(DatabaseContainersTheoryData))] + public void ImplementsIDatabaseContainerInterface(Type type) + { + Assert.True(type.IsAssignableTo(typeof(IDatabaseContainer))); + } + + public static IEnumerable DatabaseContainersTheoryData + { + get + { + static bool HasGetConnectionStringMethod(Type type) => type.IsAssignableTo(typeof(IContainer)) && type.GetMethod("GetConnectionString") != null; + var assembly = typeof(DatabaseContainersTest).Assembly; + var dependencyContext = DependencyContext.Load(assembly) ?? throw new InvalidOperationException($"DependencyContext.Load({assembly}) returned null"); + return dependencyContext.RuntimeLibraries + .Where(library => library.Name.StartsWith("Testcontainers.")) + .SelectMany(library => Assembly.Load(library.Name).GetExportedTypes().Where(HasGetConnectionStringMethod)) + .Select(type => new[] { type }); + } + } +} \ No newline at end of file diff --git a/tests/Testcontainers.Databases.Tests/Testcontainers.Databases.Tests.csproj b/tests/Testcontainers.Databases.Tests/Testcontainers.Databases.Tests.csproj new file mode 100644 index 000000000..44a081820 --- /dev/null +++ b/tests/Testcontainers.Databases.Tests/Testcontainers.Databases.Tests.csproj @@ -0,0 +1,17 @@ + + + net6.0 + false + false + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Testcontainers.Databases.Tests/Usings.cs b/tests/Testcontainers.Databases.Tests/Usings.cs new file mode 100644 index 000000000..674b17d60 --- /dev/null +++ b/tests/Testcontainers.Databases.Tests/Usings.cs @@ -0,0 +1,7 @@ +global using System; +global using System.Collections.Generic; +global using System.Linq; +global using System.Reflection; +global using DotNet.Testcontainers.Containers; +global using Microsoft.Extensions.DependencyModel; +global using Xunit; \ No newline at end of file