Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .github/scripts/Filter-TestProjects.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ $DATABASE_MODULES = @(
"Testcontainers.MySql",
"Testcontainers.Oracle",
"Testcontainers.PostgreSql",
"Testcontainers.TUnit",
"Testcontainers.Xunit",
"Testcontainers.XunitV3"
)
Expand Down
4 changes: 3 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@
<PackageVersion Include="Dapper" Version="2.1.79"/>
<PackageVersion Include="Moq" Version="4.20.72"/>
<PackageVersion Include="ReflectionMagic" Version="5.0.1"/>
<PackageVersion Include="TUnit" Version="1.68.17"/>
<PackageVersion Include="xunit.analyzers" Version="1.27.0"/>
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5"/>
<PackageVersion Include="xunit" Version="2.9.3"/>
<PackageVersion Include="xunit.v3" Version="3.2.2"/>
<!-- xUnit.net extensibility for Testcontainers.Xunit and Testcontainers.XunitV3 packages: -->
<!-- Test framework extensibility for Testcontainers.Xunit, Testcontainers.XunitV3 and Testcontainers.TUnit packages: -->
<PackageVersion Include="TUnit.Core" Version="1.68.17"/>
<PackageVersion Include="xunit.extensibility.execution" Version="2.9.3"/>
<PackageVersion Include="xunit.v3.extensibility.core" Version="3.2.2"/>
<!-- Third-party client dependencies to connect and interact with the containers: -->
Expand Down
2 changes: 2 additions & 0 deletions Testcontainers.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
<Project Path="src/Testcontainers.Sftp/Testcontainers.Sftp.csproj"/>
<Project Path="src/Testcontainers.Temporal/Testcontainers.Temporal.csproj"/>
<Project Path="src/Testcontainers.Toxiproxy/Testcontainers.Toxiproxy.csproj"/>
<Project Path="src/Testcontainers.TUnit/Testcontainers.TUnit.csproj"/>
<Project Path="src/Testcontainers.Typesense/Testcontainers.Typesense.csproj"/>
<Project Path="src/Testcontainers.Weaviate/Testcontainers.Weaviate.csproj"/>
<Project Path="src/Testcontainers.WebDriver/Testcontainers.WebDriver.csproj"/>
Expand Down Expand Up @@ -153,6 +154,7 @@
<Project Path="tests/Testcontainers.Temporal.Tests/Testcontainers.Temporal.Tests.csproj"/>
<Project Path="tests/Testcontainers.Tests/Testcontainers.Tests.csproj"/>
<Project Path="tests/Testcontainers.Toxiproxy.Tests/Testcontainers.Toxiproxy.Tests.csproj"/>
<Project Path="tests/Testcontainers.TUnit.Tests/Testcontainers.TUnit.Tests.csproj"/>
<Project Path="tests/Testcontainers.Typesense.Tests/Testcontainers.Typesense.Tests.csproj"/>
<Project Path="tests/Testcontainers.Weaviate.Tests/Testcontainers.Weaviate.Tests.csproj"/>
<Project Path="tests/Testcontainers.WebDriver.Tests/Testcontainers.WebDriver.Tests.csproj"/>
Expand Down
70 changes: 70 additions & 0 deletions build/Tasks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,18 @@ public sealed class BuildContext(ICakeContext context) : FrostingContext(context
{
internal BuildParameters Parameters { get; } = BuildParameters.Instance(context);

/// <summary>
/// Runs the tests of a test project and writes the TRX report and the code coverage to the test results directory.
/// </summary>
/// <param name="project">The test project.</param>
public void DotNetTest(SolutionProject project)
{
if (UsesTestingPlatform(project))
{
DotNetTestWithTestingPlatform(project);
return;
}

this.DotNetTest(project.Path.FullPath, new DotNetTestSettings
{
Configuration = Parameters.Configuration,
Expand All @@ -21,6 +31,64 @@ public void DotNetTest(SolutionProject project)
.AppendSwitchQuoted("--blame-hang-timeout", "10m"),
});
}

/// <summary>
/// Runs the tests of a Microsoft.Testing.Platform test project (e.g. TUnit).
/// </summary>
/// <remarks>
/// These projects cannot run through the VSTest mode of <c>dotnet test</c> on the .NET 10 SDK and later.
/// The project is run directly instead, with the platform's equivalents of the VSTest options:
/// TRX report, code coverage, results directory, and test filter.
/// </remarks>
/// <param name="project">The test project.</param>
private void DotNetTestWithTestingPlatform(SolutionProject project)
{
var resultsDirectoryPath = this.MakeAbsolute(Parameters.Paths.Directories.TestResultsDirectoryPath);

var arguments = new ProcessArgumentBuilder()
.Append("--report-trx")
.AppendSwitchQuoted("--report-trx-filename", $"{project.Name}.trx")
.Append("--coverage")
.AppendSwitch("--coverage-output-format", "xml")
.AppendSwitchQuoted("--coverage-output", $"{project.Name}.coverage.xml")
.AppendSwitchQuoted("--results-directory", resultsDirectoryPath.FullPath);

if (!string.IsNullOrEmpty(Parameters.TestFilter))
{
arguments.AppendSwitchQuoted("--treenode-filter", Parameters.TestFilter);
}

// Cake appends the verbosity after the '--' separator, which the test application does not understand.
this.DotNetRun(project.Path.FullPath, arguments, new DotNetRunSettings
{
Configuration = Parameters.Configuration,
NoRestore = true,
NoBuild = true,
});
}

/// <summary>
/// Determines whether a test project opts into running through Microsoft.Testing.Platform instead of VSTest.
/// </summary>
/// <remarks>
/// The opt-in is the MSBuild property <c>TestingPlatformDotnetTestSupport</c>. TUnit sets it to <c>true</c>; xUnit.net v3 sets it to <c>false</c>.
/// </remarks>
/// <param name="project">The test project.</param>
/// <returns><c>true</c> if the project runs through Microsoft.Testing.Platform; otherwise, <c>false</c>.</returns>
private bool UsesTestingPlatform(SolutionProject project)
{
var processSettings = new ProcessSettings
{
Arguments = new ProcessArgumentBuilder()
.Append("msbuild")
.AppendQuoted(project.Path.FullPath)
.Append("-getProperty:TestingPlatformDotnetTestSupport"),
RedirectStandardOutput = true,
};

var exitCode = this.StartProcess("dotnet", processSettings, out var output);
return exitCode == 0 && output.Any(line => "true".Equals(line.Trim(), StringComparison.OrdinalIgnoreCase));
}
}

public sealed class BuildLifetime : FrostingLifetime<BuildContext>
Expand Down Expand Up @@ -154,6 +222,8 @@ public override void Run(BuildContext context)
OpenCoverReportsPath = $"{context.MakeAbsolute(param.Paths.Directories.TestResultsDirectoryPath)}/**/*.opencover.xml",
VsTestReportsPath = $"{context.MakeAbsolute(param.Paths.Directories.TestResultsDirectoryPath)}/**/*.trx",
ArgumentCustomization = args => args
// Microsoft.Testing.Platform test projects report code coverage in the Visual Studio coverage XML format.
.Append($"/d:sonar.cs.vscoveragexml.reportsPaths=\"{context.MakeAbsolute(param.Paths.Directories.TestResultsDirectoryPath)}/**/*.coverage.xml\"")
.Append("/d:sonar.scanner.scanAll=\"false\"")
.Append("/d:sonar.scanner.skipJreProvisioning=\"true\""),
});
Expand Down
1 change: 1 addition & 0 deletions build/Usings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
global using Cake.Common.Tools.DotNet.NuGet.Push;
global using Cake.Common.Tools.DotNet.Pack;
global using Cake.Common.Tools.DotNet.Restore;
global using Cake.Common.Tools.DotNet.Run;
global using Cake.Common.Tools.DotNet.Test;
global using Cake.Common.Xml;
global using Cake.Core;
Expand Down
133 changes: 133 additions & 0 deletions docs/test_frameworks/tunit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Testing with TUnit

The [Testcontainers.TUnit](https://www.nuget.org/packages/Testcontainers.TUnit) package simplifies writing tests with containers in [TUnit](https://tunit.dev). By leveraging TUnit's [test lifecycle](https://tunit.dev/docs/writing-tests/lifecycle) and [injectable class data sources](https://tunit.dev/docs/writing-tests/class-data-source), 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.TUnit
```

## Creating an isolated test context

To create a new test resource instance for each test, inherit from the `ContainerTest<TBuilderEntity, TContainerEntity>` class. TUnit creates a new instance of the test class for every test, so 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()` 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.TUnit.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 can point to a new version.

The base class automatically forwards Testcontainers' log messages to the output of the running test. Container startup honors the test's cancellation token, so a canceled or timed-out test does not leave a container start running in the background.

Considering that each test gets its own test resource instance (Redis container), retrieving the Redis (string) value in the second test will always return `null`, regardless of the order in which TUnit runs the tests.

=== "Run Tests"
```csharp
--8<-- "tests/Testcontainers.TUnit.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.

```text 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.14.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, the same assembly, or even the entire test session.

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

TUnit injects the fixture through the `ClassDataSource<TFixture>` attribute. The `Shared` argument controls the lifetime of the fixture: `SharedType.PerClass` creates the fixture once for the entire test class, `SharedType.PerAssembly` and `SharedType.PerTestSession` widen the scope accordingly, and `SharedType.Keyed` shares the fixture among all tests that use the same key. TUnit starts the container before the first test that uses the fixture runs and disposes of it after the last test in the chosen scope completes. Add the attribute to your test class and accept the fixture as a constructor parameter, or annotate a `required` property with it instead.

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

TUnit runs tests in parallel by default. In this case, retrieving the Redis (string) value in the second test depends on the value the first test adds. The `DependsOn` attribute ensures the second test does not start before the first one has finished, without sacrificing parallelism for the remaining tests. The Redis (string) value will therefore no longer be `null`; instead, it will return the value added in the first test.

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

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

```text 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.14.0 "/bin/ryuk" 3 seconds ago
```

!!! note

TUnit sets injected properties before it initializes an instance. A fixture can therefore declare its own `ClassDataSource<TFixture>` properties (for example, a shared network or a dependent container) and use them inside `Configure()`. TUnit resolves the dependency graph, initializes the fixtures depth-first, and disposes of them in reverse order.

## 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()` 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.TUnit.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.TUnit.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. TUnit injects the test's `CancellationToken` when the test method declares a parameter of that type.

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

To share a database container across tests, inherit from `DbContainerFixture` instead. The fixture implements the `DbProviderFactory` property and the `Configure()` method in one place.

=== "Configure PostgreSQL Container Fixture"
```csharp
--8<-- "tests/Testcontainers.TUnit.Tests/PostgreSqlContainerFixture.cs:ConfigurePostgreSqlContainer"
```

Instead of a constructor parameter, this example injects the fixture through a `required` property. Both styles are supported by TUnit.

=== "Inject PostgreSQL Container Fixture"
```csharp
--8<-- "tests/Testcontainers.TUnit.Tests/PostgreSqlContainerFixture.cs:InjectContainerFixture"
```

The fixture offers the same helper methods as the test base class. `CreateCommand` and `CreateBatch` return objects that are already bound to the database and ready for execution.

=== "Run Tests"
```csharp
--8<-- "tests/Testcontainers.TUnit.Tests/PostgreSqlContainerFixture.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 @@ -51,6 +51,7 @@ nav:
- dind/index.md
- full_framework/index.md
- test_frameworks/xunit_net.md
- test_frameworks/tunit.md
- Examples:
- examples/aspnet.md
- Modules:
Expand Down
1 change: 1 addition & 0 deletions src/Testcontainers.TUnit/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root = true
23 changes: 23 additions & 0 deletions src/Testcontainers.TUnit/ContainerFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Testcontainers.TUnit;

/// <summary>
/// Fixture for sharing a container instance across multiple tests.
/// Inject the fixture with <c>[ClassDataSource&lt;TFixture&gt;(Shared = SharedType.PerClass)]</c> (or any other <see cref="SharedType" />) into the test class.
/// See <a href="https://tunit.dev/docs/writing-tests/class-data-source">Injectable Class Data Source</a> from the TUnit documentation for more information about sharing instances.
/// A logger is automatically configured to write messages to the output of the running test.
/// </summary>
/// <typeparam name="TBuilderEntity">The builder entity.</typeparam>
/// <typeparam name="TContainerEntity">The container entity.</typeparam>
[PublicAPI]
public abstract class ContainerFixture<TBuilderEntity, TContainerEntity> : ContainerLifetime<TBuilderEntity, TContainerEntity>
where TBuilderEntity : IContainerBuilder<TBuilderEntity, TContainerEntity, IContainerConfiguration>, new()
where TContainerEntity : IContainer
{
/// <summary>
/// Initializes a new instance of the <see cref="ContainerFixture{TBuilderEntity,TContainerEntity}" /> class.
/// </summary>
protected ContainerFixture()
: base(new TestContextLogger())
{
}
}
Loading
Loading