Skip to content
Merged
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
12 changes: 5 additions & 7 deletions TUnit.AspNetCore/WebApplicationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,15 @@ namespace TUnit.AspNetCore;

public abstract class WebApplicationTest
{
internal static int _idCounter;

/// <summary>
/// Gets a unique identifier for this test instance.
/// Useful for creating isolated resources (tables, topics, keys) per test.
/// Delegates to <see cref="TestContext.Isolation"/> to ensure consistency
/// regardless of whether accessed via this property or the TestContext API.
/// </summary>
public int UniqueId { get; }
public int UniqueId => TestContext.Current!.Isolation.UniqueId;

internal WebApplicationTest()
{
UniqueId = Interlocked.Increment(ref _idCounter);
}

/// <summary>
Expand All @@ -36,7 +34,7 @@ internal WebApplicationTest()
/// var topicName = GetIsolatedName("orders"); // Returns "Test_42_orders"
/// </code>
/// </example>
protected string GetIsolatedName(string baseName) => $"Test_{UniqueId}_{baseName}";
protected string GetIsolatedName(string baseName) => TestContext.Current!.Isolation.GetIsolatedName(baseName);

/// <summary>
/// Creates an isolated prefix using the test's unique identifier.
Expand All @@ -51,7 +49,7 @@ internal WebApplicationTest()
/// var dotPrefix = GetIsolatedPrefix("."); // Returns "test.42."
/// </code>
/// </example>
protected string GetIsolatedPrefix(string separator = "_") => $"test{separator}{UniqueId}{separator}";
protected string GetIsolatedPrefix(string separator = "_") => TestContext.Current!.Isolation.GetIsolatedPrefix(separator);
}

/// <summary>
Expand Down
46 changes: 46 additions & 0 deletions TUnit.Core/Interfaces/ITestIsolation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
namespace TUnit.Core.Interfaces;

/// <summary>
/// Provides helpers for creating isolated resource names per test instance.
/// Useful for database tables, queue names, cache keys, and other resources
/// that need to be unique across parallel test execution.
/// Accessed via <see cref="TestContext.Isolation"/>.
/// </summary>
public interface ITestIsolation
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing Snapshot Test Updates

This PR introduces public API changes that require updating snapshot tests per CLAUDE.md Rule 2:

Snapshot Testing - Changes to source generator output or public APIs require running snapshot tests. Commit .verified.txt files. NEVER commit .received.txt.

Public API Changes Made

  1. New public interface: ITestIsolation (this file)
  2. Modified TestContext: Now implements ITestIsolation and exposes public ITestIsolation Isolation => this;

Required Action

Run the public API snapshot tests and commit the updated .verified.txt files:

dotnet test TUnit.PublicAPI
# Review the changes in *.verified.txt files
git add TUnit.PublicAPI/**/*.verified.txt

See mandatory-rules.md for details on when snapshot tests are required.

{
/// <summary>
/// Gets a unique identifier for this test instance.
/// This value is assigned atomically and is guaranteed to be unique across all test instances in the process.
/// </summary>
int UniqueId { get; }

/// <summary>
/// Creates an isolated name by combining a base name with the test's unique identifier.
/// Use for database tables, Redis keys, Kafka topics, etc.
/// </summary>
/// <param name="baseName">The base name for the resource.</param>
/// <returns>A unique name in the format "Test_{UniqueId}_{baseName}".</returns>
/// <example>
/// <code>
/// // In a test with UniqueId = 42:
/// var tableName = TestContext.Current!.Isolation.GetIsolatedName("todos"); // Returns "Test_42_todos"
/// var topicName = TestContext.Current!.Isolation.GetIsolatedName("orders"); // Returns "Test_42_orders"
/// </code>
/// </example>
string GetIsolatedName(string baseName);

/// <summary>
/// Creates an isolated prefix using the test's unique identifier.
/// Use for key prefixes in Redis, Kafka topic prefixes, etc.
/// </summary>
/// <param name="separator">The separator character. Defaults to "_".</param>
/// <returns>A unique prefix in the format "test{separator}{UniqueId}{separator}".</returns>
/// <example>
/// <code>
/// // In a test with UniqueId = 42:
/// var prefix = TestContext.Current!.Isolation.GetIsolatedPrefix(); // Returns "test_42_"
/// var dotPrefix = TestContext.Current!.Isolation.GetIsolatedPrefix("."); // Returns "test.42."
/// </code>
/// </example>
string GetIsolatedPrefix(string separator = "_");
}
19 changes: 19 additions & 0 deletions TUnit.Core/TestContext.Isolation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using TUnit.Core.Interfaces;

namespace TUnit.Core;

public partial class TestContext
{
internal static int _isolationIdCounter;

internal int IsolationUniqueId { get; }

/// <inheritdoc/>
int ITestIsolation.UniqueId => IsolationUniqueId;

/// <inheritdoc/>
string ITestIsolation.GetIsolatedName(string baseName) => $"Test_{IsolationUniqueId}_{baseName}";

/// <inheritdoc/>
string ITestIsolation.GetIsolatedPrefix(string separator) => $"test{separator}{IsolationUniqueId}{separator}";
}
4 changes: 3 additions & 1 deletion TUnit.Core/TestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace TUnit.Core;
/// </summary>
[DebuggerDisplay("{TestDetails.ClassType.Name}.{GetDisplayName(),nq}")]
public partial class TestContext : Context,
ITestExecution, ITestParallelization, ITestOutput, ITestMetadata, ITestDependencies, ITestStateBag, ITestEvents
ITestExecution, ITestParallelization, ITestOutput, ITestMetadata, ITestDependencies, ITestStateBag, ITestEvents, ITestIsolation
{
private static readonly ConcurrentDictionary<string, TestContext> _testContextsById = new();
private readonly TestBuilderContext _testBuilderContext;
Expand All @@ -30,6 +30,7 @@ public TestContext(string testName, IServiceProvider serviceProvider, ClassHookC

// Generate unique ID for this test instance
Id = Guid.NewGuid().ToString();
IsolationUniqueId = Interlocked.Increment(ref _isolationIdCounter);

_testContextsById[Id] = this;
}
Expand All @@ -44,6 +45,7 @@ public TestContext(string testName, IServiceProvider serviceProvider, ClassHookC
public ITestDependencies Dependencies => this;
public ITestStateBag StateBag => this;
public ITestEvents Events => this;
public ITestIsolation Isolation => this;

internal IServiceProvider Services => ServiceProvider;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1327,14 +1327,15 @@ namespace
public TestConstructorAttribute() { }
}
[.DebuggerDisplay("{.Name}.{GetDisplayName(),nq}")]
public class TestContext : .Context, ., ., ., ., ., ., .
public class TestContext : .Context, ., ., ., ., ., ., ., .
{
public TestContext(string testName, serviceProvider, .ClassHookContext classContext, .TestBuilderContext testBuilderContext, .CancellationToken cancellationToken) { }
public .ClassHookContext ClassContext { get; }
public . Dependencies { get; }
public . Events { get; }
public . Execution { get; }
public string Id { get; }
public . Isolation { get; }
public object Lock { get; }
public . Metadata { get; }
public . Output { get; }
Expand Down Expand Up @@ -2477,6 +2478,12 @@ namespace .Interfaces
string TestId { get; }
string TestName { get; }
}
public interface ITestIsolation
{
int UniqueId { get; }
string GetIsolatedName(string baseName);
string GetIsolatedPrefix(string separator = "_");
}
public interface ITestLocation
{
string TestFilePath { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1327,14 +1327,15 @@ namespace
public TestConstructorAttribute() { }
}
[.DebuggerDisplay("{.Name}.{GetDisplayName(),nq}")]
public class TestContext : .Context, ., ., ., ., ., ., .
public class TestContext : .Context, ., ., ., ., ., ., ., .
{
public TestContext(string testName, serviceProvider, .ClassHookContext classContext, .TestBuilderContext testBuilderContext, .CancellationToken cancellationToken) { }
public .ClassHookContext ClassContext { get; }
public . Dependencies { get; }
public . Events { get; }
public . Execution { get; }
public string Id { get; }
public . Isolation { get; }
public object Lock { get; }
public . Metadata { get; }
public . Output { get; }
Expand Down Expand Up @@ -2477,6 +2478,12 @@ namespace .Interfaces
string TestId { get; }
string TestName { get; }
}
public interface ITestIsolation
{
int UniqueId { get; }
string GetIsolatedName(string baseName);
string GetIsolatedPrefix(string separator = "_");
}
public interface ITestLocation
{
string TestFilePath { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1327,14 +1327,15 @@ namespace
public TestConstructorAttribute() { }
}
[.DebuggerDisplay("{.Name}.{GetDisplayName(),nq}")]
public class TestContext : .Context, ., ., ., ., ., ., .
public class TestContext : .Context, ., ., ., ., ., ., ., .
{
public TestContext(string testName, serviceProvider, .ClassHookContext classContext, .TestBuilderContext testBuilderContext, .CancellationToken cancellationToken) { }
public .ClassHookContext ClassContext { get; }
public . Dependencies { get; }
public . Events { get; }
public . Execution { get; }
public string Id { get; }
public . Isolation { get; }
public object Lock { get; }
public . Metadata { get; }
public . Output { get; }
Expand Down Expand Up @@ -2477,6 +2478,12 @@ namespace .Interfaces
string TestId { get; }
string TestName { get; }
}
public interface ITestIsolation
{
int UniqueId { get; }
string GetIsolatedName(string baseName);
string GetIsolatedPrefix(string separator = "_");
}
public interface ITestLocation
{
string TestFilePath { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1281,14 +1281,15 @@ namespace
public TestConstructorAttribute() { }
}
[.DebuggerDisplay("{.Name}.{GetDisplayName(),nq}")]
public class TestContext : .Context, ., ., ., ., ., ., .
public class TestContext : .Context, ., ., ., ., ., ., ., .
{
public TestContext(string testName, serviceProvider, .ClassHookContext classContext, .TestBuilderContext testBuilderContext, .CancellationToken cancellationToken) { }
public .ClassHookContext ClassContext { get; }
public . Dependencies { get; }
public . Events { get; }
public . Execution { get; }
public string Id { get; }
public . Isolation { get; }
public object Lock { get; }
public . Metadata { get; }
public . Output { get; }
Expand Down Expand Up @@ -2427,6 +2428,12 @@ namespace .Interfaces
string TestId { get; }
string TestName { get; }
}
public interface ITestIsolation
{
int UniqueId { get; }
string GetIsolatedName(string baseName);
string GetIsolatedPrefix(string separator = "_");
}
public interface ITestLocation
{
string TestFilePath { get; }
Expand Down
114 changes: 114 additions & 0 deletions TUnit.UnitTests/TestIsolationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using TUnit.Assertions.Extensions;
using TUnit.Core.Interfaces;

namespace TUnit.UnitTests;

public class TestIsolationTests
{
[Test]
public async Task UniqueId_IsPositive()
{
var isolation = TestContext.Current!.Isolation;

await Assert.That(isolation.UniqueId).IsGreaterThan(0);
}

[Test]
public async Task UniqueId_IsDifferentAcrossTests_1()
{
// Store uniqueId in state bag so a parallel test can verify it's different
var isolation = TestContext.Current!.Isolation;
TestContext.Current.StateBag["isolation_test_id_1"] = isolation.UniqueId;

await Assert.That(isolation.UniqueId).IsGreaterThan(0);
}

[Test]
public async Task UniqueId_IsDifferentAcrossTests_2()
{
var isolation = TestContext.Current!.Isolation;
TestContext.Current.StateBag["isolation_test_id_2"] = isolation.UniqueId;

await Assert.That(isolation.UniqueId).IsGreaterThan(0);
}

[Test]
public async Task GetIsolatedName_ReturnsExpectedFormat()
{
var isolation = TestContext.Current!.Isolation;
var id = isolation.UniqueId;

var name = isolation.GetIsolatedName("foo");

await Assert.That(name).IsEqualTo($"Test_{id}_foo");
}

[Test]
public async Task GetIsolatedName_WithDifferentBaseNames_ReturnsDifferentNames()
{
var isolation = TestContext.Current!.Isolation;

var name1 = isolation.GetIsolatedName("todos");
var name2 = isolation.GetIsolatedName("orders");

await Assert.That(name1).IsNotEqualTo(name2);
await Assert.That(name1).Contains("todos");
await Assert.That(name2).Contains("orders");
}

[Test]
public async Task GetIsolatedPrefix_WithDefaultSeparator_ReturnsExpectedFormat()
{
var isolation = TestContext.Current!.Isolation;
var id = isolation.UniqueId;

var prefix = isolation.GetIsolatedPrefix();

await Assert.That(prefix).IsEqualTo($"test_{id}_");
}

[Test]
public async Task GetIsolatedPrefix_WithDotSeparator_ReturnsExpectedFormat()
{
var isolation = TestContext.Current!.Isolation;
var id = isolation.UniqueId;

var prefix = isolation.GetIsolatedPrefix(".");

await Assert.That(prefix).IsEqualTo($"test.{id}.");
}

[Test]
public async Task GetIsolatedPrefix_WithCustomSeparator_ReturnsExpectedFormat()
{
var isolation = TestContext.Current!.Isolation;
var id = isolation.UniqueId;

var prefix = isolation.GetIsolatedPrefix("-");

await Assert.That(prefix).IsEqualTo($"test-{id}-");
}

[Test]
public async Task Isolation_IsAccessibleViaInterface()
{
ITestIsolation isolation = TestContext.Current!.Isolation;

await Assert.That(isolation).IsNotNull();
await Assert.That(isolation.UniqueId).IsGreaterThan(0);
}

[Test]
public async Task Isolation_SameContextReturnsSameValues()
{
var context = TestContext.Current!;

var id1 = context.Isolation.UniqueId;
var id2 = context.Isolation.UniqueId;
var name1 = context.Isolation.GetIsolatedName("test");
var name2 = context.Isolation.GetIsolatedName("test");

await Assert.That(id1).IsEqualTo(id2);
await Assert.That(name1).IsEqualTo(name2);
}
}
4 changes: 4 additions & 0 deletions docs/docs/examples/aspnet.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,10 @@ protected override void ConfigureWebHostBuilder(IWebHostBuilder builder)

## Test Isolation Helpers

:::tip Available on All Tests
The isolation helpers (`UniqueId`, `GetIsolatedName`, `GetIsolatedPrefix`) are also available on `TestContext.Current!.Isolation` for any test — not just ASP.NET Core tests. Use `TestContext.Current!.Isolation.GetIsolatedName("resource")` when you don't inherit from `WebApplicationTest`. Both share the same counter, so IDs are unique across all test types.
:::

### GetIsolatedName

Creates a unique name for resources like database tables:
Expand Down
Loading
Loading