Skip to content
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

feat: Introduce a new Testcontainers.Xunit package #1165

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
fd154c0
Add new Testcontainers.Xunit project
0xced May 27, 2024
79e1ffe
Use Testcontainers.Xunit instead of DotNet.Testcontainers.Xunit names…
0xced May 27, 2024
b4b60e8
Use Lazy<TContainerEntity> for thread safety
0xced May 27, 2024
39df223
Share the lifetime implementation between ContainerFixture and Contai…
0xced May 27, 2024
f675189
Defer the exception (if any) when accessing the container
0xced Jun 22, 2024
7289881
Improve MessageSinkLogger and TestOutputLogger implementatinos
0xced Jun 22, 2024
dc66835
Add a new Testcontainers.XunitV3 project
0xced Aug 13, 2024
181c812
Improve design of (Db)ContainerFixture for xunit.v3
0xced Aug 14, 2024
610f7ba
Share code between logger implementations
0xced Aug 14, 2024
85120de
Fix xmldoc for xUnit.net v3
0xced Aug 14, 2024
bb58f4b
Accept null ITestOutputHelper and IMessageSink to suppress logs
0xced Aug 18, 2024
8de9b29
Revert "Improve design of (Db)ContainerFixture for xunit.v3"
0xced Aug 18, 2024
f06b8df
Fix xunit.v3.extensibility.core version
0xced Aug 19, 2024
d7b0ec5
Add "Testing with xUnit" documentation
0xced Aug 14, 2024
2c7a09b
Merge branch 'develop' into feature/Testcontainers.Xunit
HofmeisterAn Sep 7, 2024
e0a1bf8
Merge branch 'develop' into feature/Testcontainers.Xunit
HofmeisterAn Sep 15, 2024
77eab50
chore: Remove BOM, align file scoped namespace, sort sln projects
HofmeisterAn Sep 16, 2024
786801b
chore: Apply configure await consistent
HofmeisterAn Sep 16, 2024
e8fa351
feat: Add Xunit.net test project
HofmeisterAn Sep 16, 2024
34d8a49
docs: Add example (incl. source code) for ContainerTest class
HofmeisterAn Sep 16, 2024
d9960df
chore: Add cli command to install Xunit package
HofmeisterAn Sep 16, 2024
45d2fc8
chore: Add 'Creating a shared test context' section
HofmeisterAn Sep 17, 2024
c47c30d
chore: Remove BOM
HofmeisterAn Sep 17, 2024
7aa2067
chore: Show complete second test
HofmeisterAn Sep 17, 2024
a2151a7
feat: Add DB test class and fixture example
HofmeisterAn Sep 18, 2024
630a073
chore: Share logger implementation
HofmeisterAn Sep 18, 2024
313b01e
fix: Remove empty line while logging runtime info
HofmeisterAn Sep 18, 2024
4f8394f
feat: Log stack trace
HofmeisterAn Sep 18, 2024
3eb96b5
fix: Exclude Xunit test from database tests
HofmeisterAn Sep 18, 2024
0793d06
docs: Add info about IMessageSink
HofmeisterAn Sep 18, 2024
285df3b
chore: Pass logger instances to ContainerLifetime
HofmeisterAn Sep 18, 2024
2e79dd7
fix: Impl GetHashCode(), add actual image version as plain string
HofmeisterAn Sep 19, 2024
53b6290
fix: Prevent empty line from logging runtime info
HofmeisterAn Sep 19, 2024
8a286b2
Merge branch 'develop' into feature/Testcontainers.Xunit
HofmeisterAn Sep 19, 2024
ea693d8
Merge branch 'develop' into feature/Testcontainers.Xunit
HofmeisterAn Sep 20, 2024
aeb8cd5
Merge branch 'develop' into feature/Testcontainers.Xunit
HofmeisterAn Oct 2, 2024
250d6a4
Merge branch 'develop' into feature/Testcontainers.Xunit
0xced Nov 22, 2024
5448078
fix: Differentiate message format for test output helper and message …
0xced Sep 19, 2024
80b1f1d
Add default constructors for ContainerFixture and ContainerTest
0xced Nov 23, 2024
df9eb0f
Fix alphabetical test case ordering
0xced Nov 23, 2024
b77a426
Merge branch 'develop' into feature/Testcontainers.Xunit
0xced Nov 25, 2024
7717214
Revert "Add default constructors for ContainerFixture and ContainerTest"
0xced Nov 25, 2024
ef3b269
Improve the XML documentation
0xced Nov 25, 2024
64ce5b7
fix: Add XUnit test project to CI config
HofmeisterAn Nov 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ jobs:
{ name: "Testcontainers.RavenDb", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.Redis", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.Redpanda", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.WebDriver", runs-on: "ubuntu-22.04" }
{ name: "Testcontainers.WebDriver", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.Xunit", runs-on: "ubuntu-22.04" }
]

runs-on: ${{ matrix.test-projects.runs-on }}
Expand Down
3 changes: 3 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.1"/>
<PackageVersion Include="Microsoft.Extensions.Diagnostics.Testing" Version="8.10.0"/>
<PackageVersion Include="coverlet.collector" Version="6.0.2"/>
<PackageVersion Include="Dapper" Version="2.1.35"/>
<PackageVersion Include="ReflectionMagic" Version="5.0.1"/>
<PackageVersion Include="xunit.extensibility.execution" Version="2.9.0"/>
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2"/>
<PackageVersion Include="xunit.v3.extensibility.core" Version="0.2.0-pre.69"/>
<PackageVersion Include="xunit" Version="2.9.2"/>
<!-- Third-party client dependencies to connect and interact with the containers: -->
<PackageVersion Include="Apache.NMS.ActiveMQ" Version="2.1.0"/>
Expand Down
21 changes: 21 additions & 0 deletions Testcontainers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Redpanda", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.WebDriver", "src\Testcontainers.WebDriver\Testcontainers.WebDriver.csproj", "{64A87DE5-29B0-4A54-9E74-560484D8C7C0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Xunit", "src\Testcontainers.Xunit\Testcontainers.Xunit.csproj", "{380BB29B-F556-404D-B13B-CA250599C565}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.XunitV3", "src\Testcontainers.XunitV3\Testcontainers.XunitV3.csproj", "{84911C93-C2A9-46E9-AE5E-D567306589E5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers", "src\Testcontainers\Testcontainers.csproj", "{EC76857B-A3B8-4B7A-A1B0-8D867A4D1733}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.ActiveMq.Tests", "tests\Testcontainers.ActiveMq.Tests\Testcontainers.ActiveMq.Tests.csproj", "{AB93C67F-0A53-4525-AE6C-29B065820ABE}"
Expand Down Expand Up @@ -195,6 +199,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.Tests", "tests\Testcontainers.Xunit.Tests\Testcontainers.Xunit.Tests.csproj", "{E901DF14-6F05-4FC2-825A-3055FAD33561}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -372,6 +378,14 @@ Global
{64A87DE5-29B0-4A54-9E74-560484D8C7C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{64A87DE5-29B0-4A54-9E74-560484D8C7C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{64A87DE5-29B0-4A54-9E74-560484D8C7C0}.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
{84911C93-C2A9-46E9-AE5E-D567306589E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{84911C93-C2A9-46E9-AE5E-D567306589E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{84911C93-C2A9-46E9-AE5E-D567306589E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{84911C93-C2A9-46E9-AE5E-D567306589E5}.Release|Any CPU.Build.0 = Release|Any CPU
{EC76857B-A3B8-4B7A-A1B0-8D867A4D1733}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EC76857B-A3B8-4B7A-A1B0-8D867A4D1733}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EC76857B-A3B8-4B7A-A1B0-8D867A4D1733}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -568,6 +582,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
{E901DF14-6F05-4FC2-825A-3055FAD33561}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E901DF14-6F05-4FC2-825A-3055FAD33561}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E901DF14-6F05-4FC2-825A-3055FAD33561}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E901DF14-6F05-4FC2-825A-3055FAD33561}.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 @@ -612,6 +630,8 @@ Global
{BFDA179A-40EB-4CEB-B8E9-0DF32C65E2C5} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{45D6F69C-4D87-4130-AA90-0DB2F7460DAE} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{64A87DE5-29B0-4A54-9E74-560484D8C7C0} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{380BB29B-F556-404D-B13B-CA250599C565} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{84911C93-C2A9-46E9-AE5E-D567306589E5} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{EC76857B-A3B8-4B7A-A1B0-8D867A4D1733} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{AB93C67F-0A53-4525-AE6C-29B065820ABE} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{8E1E0A6D-EEBB-4455-B8E8-A55AF9B2062C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
Expand Down Expand Up @@ -661,5 +681,6 @@ Global
{9E8E6AA5-65D1-498F-BEAB-BA34723A0050} = {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}
{E901DF14-6F05-4FC2-825A-3055FAD33561} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
EndGlobalSection
EndGlobal
109 changes: 109 additions & 0 deletions docs/test_frameworks/xunit_net.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Testing with xUnit.net

The [Testcontainers.Xunit](https://www.nuget.org/packages/Testcontainers.Xunit) package simplifies writing tests with containers in [xUnit.net](https://xunit.net). By leveraging xUnit.net's [shared context](https://xunit.net/docs/shared-context), this package automates the setup and teardown of test resources, creating and disposing of containers as needed. This reduces repetitive code and avoids common patterns that developers would otherwise need to implement repeatedly.

To get started, add the following dependency to your project file:

```shell title="NuGet"
dotnet add package Testcontainers.Xunit
```

## Creating an isolated test context

To create a new test resource instance for each test, inherit from the `ContainerTest<TBuilderEntity, TContainerEntity>` class. Each test resource instance is isolated and not shared across other tests, making this approach ideal for destructive operations that could interfere with other tests. You can access the generic `TContainerEntity` container instance through the `Container` property.

The example below demonstrates how to override the `Configure(TBuilderEntity)` method and pin the image version. This method allows you to configure the container instance specifically for your test case, with all container builder methods available. If your tests rely on a Testcontainers' module, the module's default configurations will be applied.

=== "Configure a Redis Container"
```csharp
--8<-- "tests/Testcontainers.Xunit.Tests/RedisContainerTest`1.cs:ConfigureRedisContainer"
```

!!!tip
Always pin the image version to avoid flakiness. This ensures consistency and prevents unexpected behavior, as the `latest` tag may pointing to a new version.

The base class also receives an instance of xUnit.net's [ITestOutputHelper](https://xunit.net/docs/capturing-output) to capture and forward log messages to the running test.

Considering that xUnit.net runs tests in a deterministic natural sort order (like `Test1`, `Test2`, etc.), retrieving the Redis (string) value in the second test will always return `null` since a new test resource instance (Redis container) is created for each test.

=== "Run Tests"
```csharp
--8<-- "tests/Testcontainers.Xunit.Tests/RedisContainerTest`1.cs:RunTests"
```

If you check the output of `docker ps`, you will notice that three container instances in total are run, with two of them being Redis instances.

```title="List running containers"
PS C:\Sources\dotnet\testcontainers-dotnet> docker ps
CONTAINER ID IMAGE COMMAND CREATED
be115f3df138 redis:7.0 "docker-entrypoint.s…" 3 seconds ago
59349127f8c0 redis:7.0 "docker-entrypoint.s…" 4 seconds ago
45fa02b3e997 testcontainers/ryuk:0.9.0 "/bin/ryuk" 4 seconds ago
```

## Creating a shared test context

Sometimes, creating and disposing of a test resource can be an expensive operation that you do not want to repeat for every test. By inheriting from the `ContainerFixture<TBuilderEntity, TContainerEntity>` class, you can share the test resource instance across all tests within the same test class.

xUnit.net's fixture implementation does not rely on the `ITestOutputHelper` interface to capture and forward log messages; instead, it expects an implementation of `IMessageSink`. Make sure your fixture's default constructor accepts the interface implementation and forwards it to the base class.

=== "Configure Redis Container"
```csharp
--8<-- "tests/Testcontainers.Xunit.Tests/RedisContainerTest`2.cs:ConfigureRedisContainer"
```

This ensures that the fixture is created only once for the entire test class, which also improves overall test performance. You must implement the `IClassFixture<TFixture>` interface with the previously created container fixture type in your test class and add the type as argument to the default constructor.

=== "Inject Redis Container"
```csharp
--8<-- "tests/Testcontainers.Xunit.Tests/RedisContainerTest`2.cs:InjectContainerFixture"
```

In this case, retrieving the Redis (string) value in the second test will no longer return `null`. Instead, it will return the value added in the first test.

=== "Run Tests"
```csharp
--8<-- "tests/Testcontainers.Xunit.Tests/RedisContainerTest`2.cs:RunTests"
```

The output of `docker ps` shows that, instead of two Redis containers, only one runs.

```title="List running containers"
PS C:\Sources\dotnet\testcontainers-dotnet> docker ps
CONTAINER ID IMAGE COMMAND CREATED
d29a393816ce redis:7.0 "docker-entrypoint.s…" 3 seconds ago
e878f0b8f4bc testcontainers/ryuk:0.9.0 "/bin/ryuk" 3 seconds ago
```

## Testing ADO.NET services

In addition to the two mentioned base classes, the package contains two more classes: `DbContainerTest` and `DbContainerFixture`, which behave identically but offer additional convenient features when working with services accessible through an ADO.NET provider.

Inherit from either the `DbContainerTest` or `DbContainerFixture` class and override the `Configure(TBuilderEntity)` method to configure your database service.

In this example, we use the default configuration of the PostgreSQL module. The container image capabilities are used to instantiate the database, schema, and test data. During startup, the PostgreSQL container runs SQL scripts placed under the `/docker-entrypoint-initdb.d/` directory automatically.

=== "Configure PostgreSQL Container"
```csharp
--8<-- "tests/Testcontainers.Xunit.Tests/PostgreSqlContainer.cs:ConfigurePostgreSqlContainer"
```

Inheriting from the database container test or fixture class requires you to implement the abstract `DbProviderFactory` property and resolve a compatible `DbProviderFactory` according to your ADO.NET service.

=== "Configure DbProviderFactory"
```csharp
--8<-- "tests/Testcontainers.Xunit.Tests/PostgreSqlContainer.cs:ConfigureDbProviderFactory"
```

!!! note

Depending on how you initialize and access the database, it may be necessary to override the `ConnectionString` property and replace the default database name with the one actual in use.

After configuring the dependent ADO.NET service, you can add the necessary tests. In this case, we run an SQL `SELECT` statement to retrieve the first record from the `album` table.

=== "Run Tests"
```csharp
--8<-- "tests/Testcontainers.Xunit.Tests/PostgreSqlContainer.cs:RunTests"
```

--8<-- "docs/modules/_call_out_test_projects.txt"
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ nav:
- api/resource_reuse.md
- api/wait_strategies.md
- api/best_practices.md
- test_frameworks/xunit_net.md
- Examples:
- examples/dind.md
- examples/aspnet.md
Expand Down
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
15 changes: 15 additions & 0 deletions src/Testcontainers.Xunit/ContainerFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace 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">An optional <see cref="IMessageSink" /> where the logs are written to. Pass <c>null</c> to ignore logs.</param>
/// <typeparam name="TBuilderEntity">The builder entity.</typeparam>
/// <typeparam name="TContainerEntity">The container entity.</typeparam>
[PublicAPI]
public class ContainerFixture<TBuilderEntity, TContainerEntity>(IMessageSink messageSink)
: ContainerLifetime<TBuilderEntity, TContainerEntity>(new MessageSinkLogger(messageSink))
where TBuilderEntity : IContainerBuilder<TBuilderEntity, TContainerEntity>, new()
where TContainerEntity : IContainer;
91 changes: 91 additions & 0 deletions src/Testcontainers.Xunit/ContainerLifetime.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
namespace Testcontainers.Xunit;

/// <summary>
/// Base class managing the lifetime of a container.
/// </summary>
/// <typeparam name="TBuilderEntity">The builder entity.</typeparam>
/// <typeparam name="TContainerEntity">The container entity.</typeparam>
public abstract class ContainerLifetime<TBuilderEntity, TContainerEntity> : IAsyncLifetime
where TBuilderEntity : IContainerBuilder<TBuilderEntity, TContainerEntity>, new()
where TContainerEntity : IContainer
{
private readonly Lazy<TContainerEntity> _container;

[CanBeNull]
private ExceptionDispatchInfo _exception;

protected ContainerLifetime(ILogger logger)
{
_container = new Lazy<TContainerEntity>(() => Configure(new TBuilderEntity().WithLogger(logger)).Build());
}

/// <summary>
/// Gets the container instance.
/// </summary>
public TContainerEntity Container
{
get
{
_exception?.Throw();
return _container.Value;
}
}

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

#if !XUNIT_V3
/// <inheritdoc />
LifetimeTask IAsyncLifetime.DisposeAsync() => DisposeAsync();
#else
/// <inheritdoc />
LifetimeTask IAsyncDisposable.DisposeAsync() => DisposeAsync();
#endif

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

/// <inheritdoc cref="IAsyncLifetime" />
protected virtual async LifetimeTask InitializeAsync()
{
try
{
await Container.StartAsync()
.ConfigureAwait(false);
}
catch (Exception e)
{
_exception = ExceptionDispatchInfo.Capture(e);
HofmeisterAn marked this conversation as resolved.
Show resolved Hide resolved
}
}

/// <inheritdoc cref="IAsyncLifetime" />
protected virtual async LifetimeTask DisposeAsync()
{
if (_exception == null)
{
await Container.DisposeAsync()
.ConfigureAwait(false);
}
}
}
18 changes: 18 additions & 0 deletions src/Testcontainers.Xunit/ContainerTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace 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>
/// <param name="testOutputHelper">An optional <see cref="ITestOutputHelper" /> where the logs are written to. Pass <c>null</c> to ignore logs.</param>
/// <param name="configure">An optional callback to configure the container.</param>
/// <typeparam name="TBuilderEntity">The builder entity.</typeparam>
/// <typeparam name="TContainerEntity">The container entity.</typeparam>
[PublicAPI]
public abstract class ContainerTest<TBuilderEntity, TContainerEntity>(ITestOutputHelper testOutputHelper, Func<TBuilderEntity, TBuilderEntity> configure = null)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we provide the configure overload here? Wouldn't it be sufficient to expect developers to override the member?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially wrote it like that for no particular reason. We can probably get rid of it, if only for consistency with how it's done with ContainerFixture.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, I take that back, it's pretty convenient actually. For example:

public abstract class MongoDbContainerTest(ITestOutputHelper testOutputHelper, Func<MongoDbBuilder, MongoDbBuilder> configure = null)
    : ContainerTest<MongoDbBuilder, MongoDbContainer>(testOutputHelper, configure)
{
    [Fact]
    public void Test()
    {
    }

    [UsedImplicitly]
    public sealed class MongoDbDefaultConfiguration(ITestOutputHelper testOutputHelper)
        : MongoDbContainerTest(testOutputHelper);

    [UsedImplicitly]
    public sealed class MongoDbNoAuthConfiguration(ITestOutputHelper testOutputHelper)
        : MongoDbContainerTest(testOutputHelper, builder => builder.WithUsername(string.Empty).WithPassword(string.Empty));

    [UsedImplicitly]
    public sealed class MongoDbV5Configuration(ITestOutputHelper testOutputHelper)
        : MongoDbContainerTest(testOutputHelper, builder => builder.WithImage("mongo:5.0"));

    [UsedImplicitly]
    public sealed class MongoDbV4Configuration(ITestOutputHelper testOutputHelper)
        : MongoDbContainerTest(testOutputHelper, builder => builder.WithImage("mongo:4.4"));
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably an interesting use case, indeed. We should document it somehow, otherwise no one will know about the overload.

: ContainerLifetime<TBuilderEntity, TContainerEntity>(new TestOutputLogger(testOutputHelper))
where TBuilderEntity : IContainerBuilder<TBuilderEntity, TContainerEntity>, new()
where TContainerEntity : IContainer
{
protected override TBuilderEntity Configure(TBuilderEntity builder) => configure != null ? configure(builder) : builder;
}
Loading