Skip to content

Commit 680b863

Browse files
committed
Add new Testcontainers.Xunit project
1 parent 91bc71a commit 680b863

13 files changed

+432
-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+
/// <typeparam name="TBuilderEntity">The builder entity.</typeparam>
9+
/// <typeparam name="TContainerEntity">The container entity.</typeparam>
10+
[PublicAPI]
11+
public class ContainerFixture<TBuilderEntity, TContainerEntity>(IMessageSink messageSink) : IAsyncLifetime
12+
where TBuilderEntity : IContainerBuilder<TBuilderEntity, TContainerEntity>, new()
13+
where TContainerEntity : IContainer
14+
{
15+
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
16+
private readonly TBuilderEntity _builder = new TBuilderEntity().WithLogger(new MessageSinkLogger(messageSink));
17+
private TContainerEntity _container;
18+
19+
/// <summary>
20+
/// The container instance.
21+
/// </summary>
22+
public TContainerEntity Container
23+
{
24+
get
25+
{
26+
_container ??= Configure(_builder).Build();
27+
return _container;
28+
}
29+
}
30+
31+
/// <summary>
32+
/// Extension point to further configure the container instance.
33+
/// </summary>
34+
/// <example>
35+
/// <code>
36+
/// public class MariaDbRootUserFixture(IMessageSink messageSink) : DbContainerFixture&lt;MariaDbBuilder, MariaDbContainer&gt;(messageSink)
37+
/// {
38+
/// public override DbProviderFactory DbProviderFactory => MySqlConnectorFactory.Instance;
39+
///
40+
/// protected override MariaDbBuilder Configure(MariaDbBuilder builder)
41+
/// {
42+
/// return builder.WithUsername("root");
43+
/// }
44+
/// }
45+
/// </code>
46+
/// </example>
47+
/// <param name="builder">The container builder.</param>
48+
/// <returns>A configured instance of <typeparamref name="TBuilderEntity" />.</returns>
49+
protected virtual TBuilderEntity Configure(TBuilderEntity builder)
50+
{
51+
return builder;
52+
}
53+
54+
/// <summary>
55+
/// The maximum allowed time for the container to start before a timeout occurs.
56+
/// Defaults to 15 minutes.
57+
/// </summary>
58+
protected virtual TimeSpan StartTimeout { get; } = TimeSpan.FromMinutes(15);
59+
60+
/// <inheritdoc />
61+
public virtual Task InitializeAsync()
62+
{
63+
_cts.CancelAfter(StartTimeout);
64+
return Container.StartAsync(_cts.Token);
65+
}
66+
67+
/// <inheritdoc />
68+
public virtual Task DisposeAsync()
69+
{
70+
_cts.Dispose();
71+
return Container.DisposeAsync().AsTask();
72+
}
73+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
15+
16+
protected ContainerTest(ITestOutputHelper testOutputHelper, Func<TBuilderEntity, TBuilderEntity> configure = null)
17+
{
18+
var builder = new TBuilderEntity().WithLogger(new TestOutputLogger(testOutputHelper));
19+
Container = configure == null ? builder.Build() : configure(builder).Build();
20+
}
21+
22+
/// <summary>
23+
/// The container instance.
24+
/// </summary>
25+
protected TContainerEntity Container { get; }
26+
27+
/// <summary>
28+
/// The maximum allowed time for the container to start before a timeout occurs.
29+
/// Defaults to 15 minutes.
30+
/// </summary>
31+
protected virtual TimeSpan StartTimeout { get; } = TimeSpan.FromMinutes(15);
32+
33+
/// <inheritdoc />
34+
public virtual Task InitializeAsync()
35+
{
36+
_cts.CancelAfter(StartTimeout);
37+
return Container.StartAsync(_cts.Token);
38+
}
39+
40+
/// <inheritdoc />
41+
public virtual Task DisposeAsync()
42+
{
43+
_cts.Dispose();
44+
return Container.DisposeAsync().AsTask();
45+
}
46+
}
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+
public override async Task InitializeAsync()
19+
{
20+
await base.InitializeAsync();
21+
_testMethods = new DbContainerTestMethods(DbProviderFactory, ConnectionString);
22+
}
23+
24+
/// <inheritdoc />
25+
public 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+
public override async Task InitializeAsync()
24+
{
25+
await base.InitializeAsync();
26+
_testMethods = new DbContainerTestMethods(DbProviderFactory, ConnectionString);
27+
}
28+
29+
/// <inheritdoc />
30+
public 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+
}

0 commit comments

Comments
 (0)