Skip to content

Commit

Permalink
Add new Testcontainers.Xunit project
Browse files Browse the repository at this point in the history
  • Loading branch information
0xced committed May 27, 2024
1 parent 734fb52 commit 3641802
Show file tree
Hide file tree
Showing 13 changed files with 426 additions and 0 deletions.
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
<PackageVersion Include="Microsoft.Extensions.Diagnostics.Testing" Version="8.2.0"/>
<PackageVersion Include="coverlet.collector" Version="6.0.1"/>
<PackageVersion Include="xunit.extensibility.execution" Version="2.7.0"/>
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.7"/>
<PackageVersion Include="xunit" Version="2.7.0"/>
<!-- Third-party client dependencies to connect and interact with the containers: -->
Expand Down
7 changes: 7 additions & 0 deletions Testcontainers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,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.Xunit", "src\Testcontainers.Xunit\Testcontainers.Xunit.csproj", "{380BB29B-F556-404D-B13B-CA250599C565}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -568,6 +570,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
{380BB29B-F556-404D-B13B-CA250599C565}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{380BB29B-F556-404D-B13B-CA250599C565}.Debug|Any CPU.Build.0 = Debug|Any CPU
{380BB29B-F556-404D-B13B-CA250599C565}.Release|Any CPU.ActiveCfg = Release|Any CPU
{380BB29B-F556-404D-B13B-CA250599C565}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{5365F780-0E6C-41F0-B1B9-7DC34368F80C} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
Expand Down Expand Up @@ -661,5 +667,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}
{380BB29B-F556-404D-B13B-CA250599C565} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
EndGlobalSection
EndGlobal
1 change: 1 addition & 0 deletions src/Testcontainers.Xunit/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root = true
73 changes: 73 additions & 0 deletions src/Testcontainers.Xunit/ContainerFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
namespace DotNet.Testcontainers.Xunit;

/// <summary>
/// Fixture for sharing a container instance across multiple tests in a single class.
/// See <a href="https://xunit.net/docs/shared-context">Shared Context between Tests</a> from xUnit.net documentation for more information about fixtures.
/// A logger is automatically configured to write diagnostic messages to xUnit's <see cref="IMessageSink"/>.
/// </summary>
/// <param name="messageSink">The message sink used for reporting diagnostic messages.</param>
/// <typeparam name="TBuilderEntity">The builder entity.</typeparam>
/// <typeparam name="TContainerEntity">The container entity.</typeparam>
[PublicAPI]
public class ContainerFixture<TBuilderEntity, TContainerEntity>(IMessageSink messageSink) : IAsyncLifetime
where TBuilderEntity : IContainerBuilder<TBuilderEntity, TContainerEntity>, new()
where TContainerEntity : IContainer
{
private TContainerEntity _container;

/// <summary>
/// The message sink used for reporting diagnostic messages.
/// </summary>
protected IMessageSink MessageSink { get; } = messageSink;

/// <summary>
/// The container instance.
/// </summary>
public TContainerEntity Container
{
get
{
if (_container == null)
{
var containerBuilder = new TBuilderEntity().WithLogger(new MessageSinkLogger(MessageSink));
_container = Configure(containerBuilder).Build();
}
return _container;
}
}

/// <summary>
/// Extension point to further configure the container instance.
/// </summary>
/// <example>
/// <code>
/// public class MariaDbRootUserFixture(IMessageSink messageSink) : DbContainerFixture&lt;MariaDbBuilder, MariaDbContainer&gt;(messageSink)
/// {
/// public override DbProviderFactory DbProviderFactory => MySqlConnectorFactory.Instance;
///
/// protected override MariaDbBuilder Configure(MariaDbBuilder builder)
/// {
/// return builder.WithUsername("root");
/// }
/// }
/// </code>
/// </example>
/// <param name="builder">The container builder.</param>
/// <returns>A configured instance of <typeparamref name="TBuilderEntity" />.</returns>
protected virtual TBuilderEntity Configure(TBuilderEntity builder)
{
return builder;
}

/// <inheritdoc />
Task IAsyncLifetime.InitializeAsync() => InitializeAsync();

/// <inheritdoc cref="IAsyncLifetime.InitializeAsync()" />
protected virtual Task InitializeAsync() => Container.StartAsync();

/// <inheritdoc />
Task IAsyncLifetime.DisposeAsync() => DisposeAsync();

/// <inheritdoc cref="IAsyncLifetime.DisposeAsync()" />
protected virtual Task DisposeAsync() => Container.DisposeAsync().AsTask();
}
36 changes: 36 additions & 0 deletions src/Testcontainers.Xunit/ContainerTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
namespace DotNet.Testcontainers.Xunit;

/// <summary>
/// Base class for tests needing a container per test method.
/// A logger is automatically configured to write messages to xUnit's <see cref="ITestOutputHelper" />.
/// </summary>
/// <typeparam name="TBuilderEntity">The builder entity.</typeparam>
/// <typeparam name="TContainerEntity">The container entity.</typeparam>
[PublicAPI]
public abstract class ContainerTest<TBuilderEntity, TContainerEntity> : IAsyncLifetime
where TBuilderEntity : IContainerBuilder<TBuilderEntity, TContainerEntity>, new()
where TContainerEntity : IContainer
{
protected ContainerTest(ITestOutputHelper testOutputHelper, Func<TBuilderEntity, TBuilderEntity> configure = null)
{
var builder = new TBuilderEntity().WithLogger(new TestOutputLogger(testOutputHelper));
Container = configure == null ? builder.Build() : configure(builder).Build();
}

/// <summary>
/// The container instance.
/// </summary>
protected TContainerEntity Container { get; }

/// <inheritdoc />
Task IAsyncLifetime.InitializeAsync() => InitializeAsync();

/// <inheritdoc cref="IAsyncLifetime.InitializeAsync()" />
protected virtual Task InitializeAsync() => Container.StartAsync();

/// <inheritdoc />
Task IAsyncLifetime.DisposeAsync() => DisposeAsync();

/// <inheritdoc cref="IAsyncLifetime.DisposeAsync()" />
protected virtual Task DisposeAsync() => Container.DisposeAsync().AsTask();
}
63 changes: 63 additions & 0 deletions src/Testcontainers.Xunit/DbContainerFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
namespace DotNet.Testcontainers.Xunit;

/// <summary>
/// Fixture for sharing a database container instance across multiple tests in a single class.
/// See <a href="https://xunit.net/docs/shared-context">Shared Context between Tests</a> from xUnit.net documentation for more information about fixtures.
/// A logger is automatically configured to write diagnostic messages to xUnit's <see cref="IMessageSink"/>.
/// </summary>
/// <typeparam name="TBuilderEntity">The builder entity.</typeparam>
/// <typeparam name="TContainerEntity">The container entity.</typeparam>
[PublicAPI]
public abstract class DbContainerFixture<TBuilderEntity, TContainerEntity>(IMessageSink messageSink) : ContainerFixture<TBuilderEntity, TContainerEntity>(messageSink), IDbContainerTestMethods
where TBuilderEntity : IContainerBuilder<TBuilderEntity, TContainerEntity>, new()
where TContainerEntity : IContainer, IDatabaseContainer
{
private DbContainerTestMethods _testMethods;

/// <inheritdoc />
protected override async Task InitializeAsync()
{
await base.InitializeAsync();
_testMethods = new DbContainerTestMethods(DbProviderFactory, ConnectionString);
}

/// <inheritdoc />
protected override async Task DisposeAsync()
{
if (_testMethods != null)
{
await _testMethods.DisposeAsync()
.ConfigureAwait(true);
}

await base.DisposeAsync()
.ConfigureAwait(true);
}

/// <summary>
/// The <see cref="DbProviderFactory"/> used to create <see cref="DbConnection"/> instances.
/// </summary>
public abstract DbProviderFactory DbProviderFactory { get; }

/// <summary>
/// Gets the database connection string.
/// </summary>
public virtual string ConnectionString => Container.GetConnectionString();

/// <inheritdoc />
public DbConnection CreateConnection() => _testMethods.CreateConnection();

#if NET8_0_OR_GREATER
/// <inheritdoc />
public DbConnection OpenConnection() => _testMethods.OpenConnection();

/// <inheritdoc />
public ValueTask<DbConnection> OpenConnectionAsync(CancellationToken cancellationToken = default) => _testMethods.OpenConnectionAsync(cancellationToken);

/// <inheritdoc />
public DbCommand CreateCommand(string commandText = null) => _testMethods.CreateCommand(commandText);

/// <inheritdoc />
public DbBatch CreateBatch() => _testMethods.CreateBatch();
#endif
}
68 changes: 68 additions & 0 deletions src/Testcontainers.Xunit/DbContainerTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
namespace DotNet.Testcontainers.Xunit;

/// <summary>
/// Base class for tests needing a database container per test method.
/// A logger is automatically configured to write messages to xUnit's <see cref="ITestOutputHelper"/>.
/// </summary>
/// <typeparam name="TBuilderEntity">The builder entity.</typeparam>
/// <typeparam name="TContainerEntity">The container entity.</typeparam>
[PublicAPI]
public abstract class DbContainerTest<TBuilderEntity, TContainerEntity> : ContainerTest<TBuilderEntity, TContainerEntity>, IDbContainerTestMethods
where TBuilderEntity : IContainerBuilder<TBuilderEntity, TContainerEntity>, new()
where TContainerEntity : IContainer, IDatabaseContainer
{
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
private DbContainerTestMethods _testMethods;

protected DbContainerTest(ITestOutputHelper testOutputHelper, Func<TBuilderEntity, TBuilderEntity> configure = null)
: base(testOutputHelper, configure)
{
}

/// <inheritdoc />
protected override async Task InitializeAsync()
{
await base.InitializeAsync();
_testMethods = new DbContainerTestMethods(DbProviderFactory, ConnectionString);
}

/// <inheritdoc />
protected override async Task DisposeAsync()
{
if (_testMethods != null)
{
await _testMethods.DisposeAsync()
.ConfigureAwait(true);
}

await base.DisposeAsync()
.ConfigureAwait(true);
}

/// <summary>
/// The <see cref="DbProviderFactory"/> used to create <see cref="DbConnection"/> instances.
/// </summary>
public abstract DbProviderFactory DbProviderFactory { get; }

/// <summary>
/// Gets the database connection string.
/// </summary>
public virtual string ConnectionString => Container.GetConnectionString();

/// <inheritdoc />
public DbConnection CreateConnection() => _testMethods.CreateConnection();

#if NET8_0_OR_GREATER
/// <inheritdoc />
public DbConnection OpenConnection() => _testMethods.OpenConnection();

/// <inheritdoc />
public ValueTask<DbConnection> OpenConnectionAsync(CancellationToken cancellationToken = default) => _testMethods.OpenConnectionAsync(cancellationToken);

/// <inheritdoc />
public DbCommand CreateCommand(string commandText = null) => _testMethods.CreateCommand(commandText);

/// <inheritdoc />
public DbBatch CreateBatch() => _testMethods.CreateBatch();
#endif
}
41 changes: 41 additions & 0 deletions src/Testcontainers.Xunit/DbContainerTestMethods.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace DotNet.Testcontainers.Xunit;

internal class DbContainerTestMethods(DbProviderFactory dbProviderFactory, string connectionString) : IDbContainerTestMethods, IAsyncDisposable
{
private readonly DbProviderFactory _dbProviderFactory = dbProviderFactory ?? throw new ArgumentNullException(nameof(dbProviderFactory));
private readonly string _connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));

#if NET8_0_OR_GREATER
[CanBeNull]
private DbDataSource _dbDataSource;
private DbDataSource DbDataSource
{
get
{
_dbDataSource ??= _dbProviderFactory.CreateDataSource(_connectionString);
return _dbDataSource;
}
}

public DbConnection CreateConnection() => DbDataSource.CreateConnection();

public DbConnection OpenConnection() => DbDataSource.OpenConnection();

public ValueTask<DbConnection> OpenConnectionAsync(CancellationToken cancellationToken = default) => DbDataSource.OpenConnectionAsync(cancellationToken);

public DbCommand CreateCommand(string commandText = null) => DbDataSource.CreateCommand(commandText);

public DbBatch CreateBatch() => DbDataSource.CreateBatch();

public ValueTask DisposeAsync() => _dbDataSource?.DisposeAsync() ?? ValueTask.CompletedTask;
#else
public DbConnection CreateConnection()
{
var connection = _dbProviderFactory.CreateConnection() ?? throw new InvalidOperationException($"DbProviderFactory.CreateConnection() returned null for {_dbProviderFactory}");
connection.ConnectionString = _connectionString;
return connection;
}

public ValueTask DisposeAsync() => default;
#endif
}
63 changes: 63 additions & 0 deletions src/Testcontainers.Xunit/IDbContainerTestMethods.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
namespace DotNet.Testcontainers.Xunit;

/// <summary>
/// Method to ease working with DbConnection, DbCommand and DbBatch provided by both
/// <see cref="DbContainerFixture{TBuilderEntity,TContainerEntity}"/> and <see cref="DbContainerTest{TBuilderEntity,TContainerEntity}"/>.
/// </summary>
internal interface IDbContainerTestMethods
{
/// <summary>
/// Returns a new, closed connection to the database.
/// </summary>
/// <remarks>
/// The connection must be opened before it can be used.
/// <para />
/// It is the responsibility of the caller to properly dispose the connection returned by this method. Failure to do so may result in a connection leak.
/// </remarks>
/// <returns>A new, closed connection to the database.</returns>
DbConnection CreateConnection();

#if NET8_0_OR_GREATER
/// <summary>
/// Returns a new, open connection to the database.
/// </summary>
/// <remarks>
/// The returned connection is already open, and is ready for immediate use.
/// <para />
/// It is the responsibility of the caller to properly dispose the connection returned by this method. Failure to do so may result in a connection leak.
/// </remarks>
/// <returns>A new, open connection to the database represented.</returns>
DbConnection OpenConnection();

/// <summary>
/// Asynchronously returns a new, open connection to the database.
/// </summary>
/// <remarks>
/// The returned connection is already open, and is ready for immediate use.
/// <para />
/// It is the responsibility of the caller to properly dispose the connection returned by this method. Failure to do so may result in a connection leak.
/// </remarks>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A new, open connection to the database.</returns>
ValueTask<DbConnection> OpenConnectionAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Returns a <see cref="DbCommand" /> that's ready for execution against the database.
/// </summary>
/// <remarks>
/// Commands returned from this method are already configured to execute against the database; their <see cref="DbCommand.Connection"/> does not need to be set, and doing so will throw an exception.
/// </remarks>
/// <param name="commandText">The text command with which to initialize the <see cref="DbCommand" /> that this method returns.</param>
/// <returns>A <see cref="DbCommand" /> that's ready for execution against the database.</returns>
DbCommand CreateCommand([CanBeNull] string commandText = null);

/// <summary>
/// Returns a <see cref="DbBatch" /> that's ready for execution against the database.
/// </summary>
/// <remarks>
/// Batches returned from this method are already configured to execute against the database; their <see cref="DbCommand.Connection"/> does not need to be set, and doing so will throw an exception.
/// </remarks>
/// <returns>A <see cref="DbBatch" /> that's ready for execution against the database.</returns>
DbBatch CreateBatch();
#endif
}
Loading

0 comments on commit 3641802

Please sign in to comment.