Skip to content

Commit 3641802

Browse files
committed
Add new Testcontainers.Xunit project
1 parent 734fb52 commit 3641802

13 files changed

+426
-0
lines changed

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
1919
<PackageVersion Include="Microsoft.Extensions.Diagnostics.Testing" Version="8.2.0"/>
2020
<PackageVersion Include="coverlet.collector" Version="6.0.1"/>
21+
<PackageVersion Include="xunit.extensibility.execution" Version="2.7.0"/>
2122
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.7"/>
2223
<PackageVersion Include="xunit" Version="2.7.0"/>
2324
<!-- Third-party client dependencies to connect and interact with the containers: -->

Testcontainers.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Tests", "tes
195195
EndProject
196196
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.WebDriver.Tests", "tests\Testcontainers.WebDriver.Tests\Testcontainers.WebDriver.Tests.csproj", "{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}"
197197
EndProject
198+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Xunit", "src\Testcontainers.Xunit\Testcontainers.Xunit.csproj", "{380BB29B-F556-404D-B13B-CA250599C565}"
199+
EndProject
198200
Global
199201
GlobalSection(SolutionConfigurationPlatforms) = preSolution
200202
Debug|Any CPU = Debug|Any CPU
@@ -568,6 +570,10 @@ Global
568570
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
569571
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
570572
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.Build.0 = Release|Any CPU
573+
{380BB29B-F556-404D-B13B-CA250599C565}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
574+
{380BB29B-F556-404D-B13B-CA250599C565}.Debug|Any CPU.Build.0 = Debug|Any CPU
575+
{380BB29B-F556-404D-B13B-CA250599C565}.Release|Any CPU.ActiveCfg = Release|Any CPU
576+
{380BB29B-F556-404D-B13B-CA250599C565}.Release|Any CPU.Build.0 = Release|Any CPU
571577
EndGlobalSection
572578
GlobalSection(NestedProjects) = preSolution
573579
{5365F780-0E6C-41F0-B1B9-7DC34368F80C} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
@@ -661,5 +667,6 @@ Global
661667
{1A1983E6-5297-435F-B467-E8E1F11277D6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
662668
{27CDB869-A150-4593-958F-6F26E5391E7C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
663669
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
670+
{380BB29B-F556-404D-B13B-CA250599C565} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
664671
EndGlobalSection
665672
EndGlobal
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
root = true
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
namespace DotNet.Testcontainers.Xunit;
2+
3+
/// <summary>
4+
/// Fixture for sharing a container instance across multiple tests in a single class.
5+
/// See <a href="https://xunit.net/docs/shared-context">Shared Context between Tests</a> from xUnit.net documentation for more information about fixtures.
6+
/// A logger is automatically configured to write diagnostic messages to xUnit's <see cref="IMessageSink"/>.
7+
/// </summary>
8+
/// <param name="messageSink">The message sink used for reporting diagnostic messages.</param>
9+
/// <typeparam name="TBuilderEntity">The builder entity.</typeparam>
10+
/// <typeparam name="TContainerEntity">The container entity.</typeparam>
11+
[PublicAPI]
12+
public class ContainerFixture<TBuilderEntity, TContainerEntity>(IMessageSink messageSink) : IAsyncLifetime
13+
where TBuilderEntity : IContainerBuilder<TBuilderEntity, TContainerEntity>, new()
14+
where TContainerEntity : IContainer
15+
{
16+
private TContainerEntity _container;
17+
18+
/// <summary>
19+
/// The message sink used for reporting diagnostic messages.
20+
/// </summary>
21+
protected IMessageSink MessageSink { get; } = messageSink;
22+
23+
/// <summary>
24+
/// The container instance.
25+
/// </summary>
26+
public TContainerEntity Container
27+
{
28+
get
29+
{
30+
if (_container == null)
31+
{
32+
var containerBuilder = new TBuilderEntity().WithLogger(new MessageSinkLogger(MessageSink));
33+
_container = Configure(containerBuilder).Build();
34+
}
35+
return _container;
36+
}
37+
}
38+
39+
/// <summary>
40+
/// Extension point to further configure the container instance.
41+
/// </summary>
42+
/// <example>
43+
/// <code>
44+
/// public class MariaDbRootUserFixture(IMessageSink messageSink) : DbContainerFixture&lt;MariaDbBuilder, MariaDbContainer&gt;(messageSink)
45+
/// {
46+
/// public override DbProviderFactory DbProviderFactory => MySqlConnectorFactory.Instance;
47+
///
48+
/// protected override MariaDbBuilder Configure(MariaDbBuilder builder)
49+
/// {
50+
/// return builder.WithUsername("root");
51+
/// }
52+
/// }
53+
/// </code>
54+
/// </example>
55+
/// <param name="builder">The container builder.</param>
56+
/// <returns>A configured instance of <typeparamref name="TBuilderEntity" />.</returns>
57+
protected virtual TBuilderEntity Configure(TBuilderEntity builder)
58+
{
59+
return builder;
60+
}
61+
62+
/// <inheritdoc />
63+
Task IAsyncLifetime.InitializeAsync() => InitializeAsync();
64+
65+
/// <inheritdoc cref="IAsyncLifetime.InitializeAsync()" />
66+
protected virtual Task InitializeAsync() => Container.StartAsync();
67+
68+
/// <inheritdoc />
69+
Task IAsyncLifetime.DisposeAsync() => DisposeAsync();
70+
71+
/// <inheritdoc cref="IAsyncLifetime.DisposeAsync()" />
72+
protected virtual Task DisposeAsync() => Container.DisposeAsync().AsTask();
73+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
namespace DotNet.Testcontainers.Xunit;
2+
3+
/// <summary>
4+
/// Base class for tests needing a container per test method.
5+
/// A logger is automatically configured to write messages to xUnit's <see cref="ITestOutputHelper" />.
6+
/// </summary>
7+
/// <typeparam name="TBuilderEntity">The builder entity.</typeparam>
8+
/// <typeparam name="TContainerEntity">The container entity.</typeparam>
9+
[PublicAPI]
10+
public abstract class ContainerTest<TBuilderEntity, TContainerEntity> : IAsyncLifetime
11+
where TBuilderEntity : IContainerBuilder<TBuilderEntity, TContainerEntity>, new()
12+
where TContainerEntity : IContainer
13+
{
14+
protected ContainerTest(ITestOutputHelper testOutputHelper, Func<TBuilderEntity, TBuilderEntity> configure = null)
15+
{
16+
var builder = new TBuilderEntity().WithLogger(new TestOutputLogger(testOutputHelper));
17+
Container = configure == null ? builder.Build() : configure(builder).Build();
18+
}
19+
20+
/// <summary>
21+
/// The container instance.
22+
/// </summary>
23+
protected TContainerEntity Container { get; }
24+
25+
/// <inheritdoc />
26+
Task IAsyncLifetime.InitializeAsync() => InitializeAsync();
27+
28+
/// <inheritdoc cref="IAsyncLifetime.InitializeAsync()" />
29+
protected virtual Task InitializeAsync() => Container.StartAsync();
30+
31+
/// <inheritdoc />
32+
Task IAsyncLifetime.DisposeAsync() => DisposeAsync();
33+
34+
/// <inheritdoc cref="IAsyncLifetime.DisposeAsync()" />
35+
protected virtual Task DisposeAsync() => Container.DisposeAsync().AsTask();
36+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
namespace DotNet.Testcontainers.Xunit;
2+
3+
/// <summary>
4+
/// Fixture for sharing a database container instance across multiple tests in a single class.
5+
/// See <a href="https://xunit.net/docs/shared-context">Shared Context between Tests</a> from xUnit.net documentation for more information about fixtures.
6+
/// A logger is automatically configured to write diagnostic messages to xUnit's <see cref="IMessageSink"/>.
7+
/// </summary>
8+
/// <typeparam name="TBuilderEntity">The builder entity.</typeparam>
9+
/// <typeparam name="TContainerEntity">The container entity.</typeparam>
10+
[PublicAPI]
11+
public abstract class DbContainerFixture<TBuilderEntity, TContainerEntity>(IMessageSink messageSink) : ContainerFixture<TBuilderEntity, TContainerEntity>(messageSink), IDbContainerTestMethods
12+
where TBuilderEntity : IContainerBuilder<TBuilderEntity, TContainerEntity>, new()
13+
where TContainerEntity : IContainer, IDatabaseContainer
14+
{
15+
private DbContainerTestMethods _testMethods;
16+
17+
/// <inheritdoc />
18+
protected override async Task InitializeAsync()
19+
{
20+
await base.InitializeAsync();
21+
_testMethods = new DbContainerTestMethods(DbProviderFactory, ConnectionString);
22+
}
23+
24+
/// <inheritdoc />
25+
protected override async Task DisposeAsync()
26+
{
27+
if (_testMethods != null)
28+
{
29+
await _testMethods.DisposeAsync()
30+
.ConfigureAwait(true);
31+
}
32+
33+
await base.DisposeAsync()
34+
.ConfigureAwait(true);
35+
}
36+
37+
/// <summary>
38+
/// The <see cref="DbProviderFactory"/> used to create <see cref="DbConnection"/> instances.
39+
/// </summary>
40+
public abstract DbProviderFactory DbProviderFactory { get; }
41+
42+
/// <summary>
43+
/// Gets the database connection string.
44+
/// </summary>
45+
public virtual string ConnectionString => Container.GetConnectionString();
46+
47+
/// <inheritdoc />
48+
public DbConnection CreateConnection() => _testMethods.CreateConnection();
49+
50+
#if NET8_0_OR_GREATER
51+
/// <inheritdoc />
52+
public DbConnection OpenConnection() => _testMethods.OpenConnection();
53+
54+
/// <inheritdoc />
55+
public ValueTask<DbConnection> OpenConnectionAsync(CancellationToken cancellationToken = default) => _testMethods.OpenConnectionAsync(cancellationToken);
56+
57+
/// <inheritdoc />
58+
public DbCommand CreateCommand(string commandText = null) => _testMethods.CreateCommand(commandText);
59+
60+
/// <inheritdoc />
61+
public DbBatch CreateBatch() => _testMethods.CreateBatch();
62+
#endif
63+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
namespace DotNet.Testcontainers.Xunit;
2+
3+
/// <summary>
4+
/// Base class for tests needing a database container per test method.
5+
/// A logger is automatically configured to write messages to xUnit's <see cref="ITestOutputHelper"/>.
6+
/// </summary>
7+
/// <typeparam name="TBuilderEntity">The builder entity.</typeparam>
8+
/// <typeparam name="TContainerEntity">The container entity.</typeparam>
9+
[PublicAPI]
10+
public abstract class DbContainerTest<TBuilderEntity, TContainerEntity> : ContainerTest<TBuilderEntity, TContainerEntity>, IDbContainerTestMethods
11+
where TBuilderEntity : IContainerBuilder<TBuilderEntity, TContainerEntity>, new()
12+
where TContainerEntity : IContainer, IDatabaseContainer
13+
{
14+
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
15+
private DbContainerTestMethods _testMethods;
16+
17+
protected DbContainerTest(ITestOutputHelper testOutputHelper, Func<TBuilderEntity, TBuilderEntity> configure = null)
18+
: base(testOutputHelper, configure)
19+
{
20+
}
21+
22+
/// <inheritdoc />
23+
protected override async Task InitializeAsync()
24+
{
25+
await base.InitializeAsync();
26+
_testMethods = new DbContainerTestMethods(DbProviderFactory, ConnectionString);
27+
}
28+
29+
/// <inheritdoc />
30+
protected override async Task DisposeAsync()
31+
{
32+
if (_testMethods != null)
33+
{
34+
await _testMethods.DisposeAsync()
35+
.ConfigureAwait(true);
36+
}
37+
38+
await base.DisposeAsync()
39+
.ConfigureAwait(true);
40+
}
41+
42+
/// <summary>
43+
/// The <see cref="DbProviderFactory"/> used to create <see cref="DbConnection"/> instances.
44+
/// </summary>
45+
public abstract DbProviderFactory DbProviderFactory { get; }
46+
47+
/// <summary>
48+
/// Gets the database connection string.
49+
/// </summary>
50+
public virtual string ConnectionString => Container.GetConnectionString();
51+
52+
/// <inheritdoc />
53+
public DbConnection CreateConnection() => _testMethods.CreateConnection();
54+
55+
#if NET8_0_OR_GREATER
56+
/// <inheritdoc />
57+
public DbConnection OpenConnection() => _testMethods.OpenConnection();
58+
59+
/// <inheritdoc />
60+
public ValueTask<DbConnection> OpenConnectionAsync(CancellationToken cancellationToken = default) => _testMethods.OpenConnectionAsync(cancellationToken);
61+
62+
/// <inheritdoc />
63+
public DbCommand CreateCommand(string commandText = null) => _testMethods.CreateCommand(commandText);
64+
65+
/// <inheritdoc />
66+
public DbBatch CreateBatch() => _testMethods.CreateBatch();
67+
#endif
68+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
namespace DotNet.Testcontainers.Xunit;
2+
3+
internal class DbContainerTestMethods(DbProviderFactory dbProviderFactory, string connectionString) : IDbContainerTestMethods, IAsyncDisposable
4+
{
5+
private readonly DbProviderFactory _dbProviderFactory = dbProviderFactory ?? throw new ArgumentNullException(nameof(dbProviderFactory));
6+
private readonly string _connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
7+
8+
#if NET8_0_OR_GREATER
9+
[CanBeNull]
10+
private DbDataSource _dbDataSource;
11+
private DbDataSource DbDataSource
12+
{
13+
get
14+
{
15+
_dbDataSource ??= _dbProviderFactory.CreateDataSource(_connectionString);
16+
return _dbDataSource;
17+
}
18+
}
19+
20+
public DbConnection CreateConnection() => DbDataSource.CreateConnection();
21+
22+
public DbConnection OpenConnection() => DbDataSource.OpenConnection();
23+
24+
public ValueTask<DbConnection> OpenConnectionAsync(CancellationToken cancellationToken = default) => DbDataSource.OpenConnectionAsync(cancellationToken);
25+
26+
public DbCommand CreateCommand(string commandText = null) => DbDataSource.CreateCommand(commandText);
27+
28+
public DbBatch CreateBatch() => DbDataSource.CreateBatch();
29+
30+
public ValueTask DisposeAsync() => _dbDataSource?.DisposeAsync() ?? ValueTask.CompletedTask;
31+
#else
32+
public DbConnection CreateConnection()
33+
{
34+
var connection = _dbProviderFactory.CreateConnection() ?? throw new InvalidOperationException($"DbProviderFactory.CreateConnection() returned null for {_dbProviderFactory}");
35+
connection.ConnectionString = _connectionString;
36+
return connection;
37+
}
38+
39+
public ValueTask DisposeAsync() => default;
40+
#endif
41+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
namespace DotNet.Testcontainers.Xunit;
2+
3+
/// <summary>
4+
/// Method to ease working with DbConnection, DbCommand and DbBatch provided by both
5+
/// <see cref="DbContainerFixture{TBuilderEntity,TContainerEntity}"/> and <see cref="DbContainerTest{TBuilderEntity,TContainerEntity}"/>.
6+
/// </summary>
7+
internal interface IDbContainerTestMethods
8+
{
9+
/// <summary>
10+
/// Returns a new, closed connection to the database.
11+
/// </summary>
12+
/// <remarks>
13+
/// The connection must be opened before it can be used.
14+
/// <para />
15+
/// 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.
16+
/// </remarks>
17+
/// <returns>A new, closed connection to the database.</returns>
18+
DbConnection CreateConnection();
19+
20+
#if NET8_0_OR_GREATER
21+
/// <summary>
22+
/// Returns a new, open connection to the database.
23+
/// </summary>
24+
/// <remarks>
25+
/// The returned connection is already open, and is ready for immediate use.
26+
/// <para />
27+
/// 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.
28+
/// </remarks>
29+
/// <returns>A new, open connection to the database represented.</returns>
30+
DbConnection OpenConnection();
31+
32+
/// <summary>
33+
/// Asynchronously returns a new, open connection to the database.
34+
/// </summary>
35+
/// <remarks>
36+
/// The returned connection is already open, and is ready for immediate use.
37+
/// <para />
38+
/// 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.
39+
/// </remarks>
40+
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
41+
/// <returns>A new, open connection to the database.</returns>
42+
ValueTask<DbConnection> OpenConnectionAsync(CancellationToken cancellationToken = default);
43+
44+
/// <summary>
45+
/// Returns a <see cref="DbCommand" /> that's ready for execution against the database.
46+
/// </summary>
47+
/// <remarks>
48+
/// 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.
49+
/// </remarks>
50+
/// <param name="commandText">The text command with which to initialize the <see cref="DbCommand" /> that this method returns.</param>
51+
/// <returns>A <see cref="DbCommand" /> that's ready for execution against the database.</returns>
52+
DbCommand CreateCommand([CanBeNull] string commandText = null);
53+
54+
/// <summary>
55+
/// Returns a <see cref="DbBatch" /> that's ready for execution against the database.
56+
/// </summary>
57+
/// <remarks>
58+
/// 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.
59+
/// </remarks>
60+
/// <returns>A <see cref="DbBatch" /> that's ready for execution against the database.</returns>
61+
DbBatch CreateBatch();
62+
#endif
63+
}

0 commit comments

Comments
 (0)