dotnet-tunit
Write, run, or repair .NET tests that use TUnit. Use when a repo uses `TUnit`, `TUnit.Playwright`, `[Test]`, `[Arguments]`, `ClassDataSource`, `SharedType.PerTestSession`, or Microsoft.Testing.Platform-based execution.
Install
npx skills add https://github.com/Postpartum-genushyacinthus29/dotnet-skills/tree/main/skills/dotnet-tunit
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install postpartum-genushyacinthus29-dotnet-skills@llmmart
git clone https://github.com/Postpartum-genushyacinthus29/dotnet-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole postpartum-genushyacinthus29/dotnet-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
TUnit
Trigger On
- the repo uses TUnit
- you need to add, run, debug, or repair TUnit tests
- the repo uses Microsoft.Testing.Platform-based test execution
- the repo uses
ClassDataSource<...>(Shared = SharedType.PerTestSession),ParallelLimiter,TUnit.Playwright, or--treenode-filter
Value
- produce a concrete project delta: code, docs, config, tests, CI, or review artifact
- reduce ambiguity through explicit planning, verification, and final validation skills
- leave reusable project context so future tasks are faster and safer
Do Not Use For
- xUnit projects
- MSTest projects
- generic test strategy with no TUnit-specific mechanics
Inputs
- the nearest
AGENTS.md - the test project file and package references
- the repo's current TUnit execution command
Quick Start
- Read the nearest
AGENTS.mdand confirm scope and constraints. - Run this skill's
Workflowthrough theRalph Loopuntil outcomes are acceptable. - Return the
Required Result Formatwith concrete artifacts and verification evidence.
Workflow
- Confirm the project really uses TUnit and not a different MTP-based framework.
- Read the repo's real
testcommand fromAGENTS.md. If the repo has no explicit command yet, start withdotnet test PROJECT_OR_SOLUTION. - Keep the TUnit execution model intact:
- tests are source-generated at build time
- tests run in parallel by default
- built-in analyzers should remain enabled
- Choose the fixture level deliberately:
- plain TUnit tests for isolated logic
- shared AppHost/Aspire fixtures for HTTP, SignalR, SSE, or UI flows
WebApplicationFactorylayered over shared Aspire infra when tests need Host DI services,IGrainFactory, or other runtime internals
- Reuse expensive fixtures with
ClassDataSource<Fixture>(Shared = SharedType.PerTestSession)instead of booting distributed infrastructure per test. - Fix isolation bugs instead of globally serializing the suite unless the repo already documented a justified exception.
- Run the narrowest useful scope first with
dotnet test ... -- --treenode-filter "...". Keep TUnit arguments after--. - Capture useful failure evidence: host log dumps, focused console output, coverage files, and Playwright screenshots/HTML for UI tests.
- Use
[Test],[Arguments], hooks, and dependencies only when they make the scenario clearer, not because the framework allows it.
Bootstrap When Missing
If TUnit is requested but not configured yet:
- Detect current state:
rg -n "TUnit|Microsoft\\.Testing\\.Platform" -g '*.csproj' -g 'Directory.Build.*' .
- Add the minimal package set to the test project:
dotnet add TEST_PROJECT.csproj package TUnit- add
Microsoft.NET.Test.Sdkonly when the repo's chosen TUnit project shape requires it; do not blindly duplicate runner packages
- Keep the runner model explicit in
AGENTS.mdand CI:- record that the repo uses Microsoft.Testing.Platform-compatible execution for this test project
- record the exact
dotnet test TEST_PROJECT.csprojcommand the repo will use
- Add one small executable test using
[Test]. - Run
dotnet test TEST_PROJECT.csprojand returnstatus: configuredorstatus: improved. - If the repo intentionally standardizes on xUnit or MSTest, return
status: not_applicableunless migration is explicitly requested.
Deliver
- TUnit tests that respect source generation and parallel execution
- commands that work in local and CI runs
- framework-specific verification guidance for the repo
- a fixture strategy that matches the actual test scope: logic-only, AppHost/API, Host DI/grains, or Playwright UI
Validate
- the command matches the repo's TUnit runner style
- focused runs use
--treenode-filterrather than VSTest-style--filter - shared distributed fixtures use
SharedType.PerTestSessionor an equivalent reuse pattern - shared state is isolated or explicitly controlled
- built-in TUnit analyzers remain active
- coverage tooling matches Microsoft.Testing.Platform if coverage is enabled
- UI failures capture artifacts and server-side failures expose enough logs to avoid blind reruns
Test Harness
flowchart LR
A["TUnit task"] --> B{"What does the test need?"}
B -->|"Single component only"| C["Plain TUnit test"]
B -->|"HTTP / SignalR / resource graph"| D["Shared Aspire/AppHost fixture"]
B -->|"Host DI / grains / runtime services"| E["Shared Aspire/AppHost fixture + WebApplicationFactory"]
B -->|"Browser automation"| F["Shared Aspire/AppHost fixture + Playwright"]
C & D & E & F --> G["Run focused with --treenode-filter"]
G --> H["Capture logs, artifacts, and coverage"]
Ralph Loop
Use the Ralph Loop for every task, including docs, architecture, testing, and tooling work.
- Plan first (mandatory):
- analyze current state
- define target outcome, constraints, and risks
- write a detailed execution plan
- list final validation skills to run at the end, with order and reason
- Execute one planned step and produce a concrete delta.
- Review the result and capture findings with actionable next fixes.
- Apply fixes in small batches and rerun the relevant checks or review steps.
- Update the plan after each iteration.
- Repeat until outcomes are acceptable or only explicit exceptions remain.
- If a dependency is missing, bootstrap it or return
status: not_applicablewith explicit reason and fallback path.
Required Result Format
status:complete|clean|improved|configured|not_applicable|blockedplan: concise plan and current iteration stepactions_taken: concrete changes madevalidation_skills: final skills run, or skipped with reasonsverification: commands, checks, or review evidence summaryremaining: top unresolved items ornone
For setup-only requests with no execution, return status: configured and exact next commands.
Load References
references/patterns.mdreferences/migration.mdreferences/tunit.mdreferences/integration-testing.md
Running Tests
TUnit uses Microsoft.Testing.Platform. Use --treenode-filter for filtering (not --filter), and keep runner switches after --.
# Run all tests
dotnet test MySolution.sln
# Run one test project
dotnet test tests/MyProject.Tests/MyProject.Tests.csproj
# Filter by class
dotnet test tests/MyProject.Tests/MyProject.Tests.csproj -- --treenode-filter "/*/*/CalculatorTests/*"
# Filter by category
dotnet test tests/MyProject.Tests/MyProject.Tests.csproj -- --treenode-filter "/*/*/*/*[Category=Integration]"
# Coverage on Microsoft.Testing.Platform
dotnet test MySolution.sln -- --coverage --coverage-output coverage.cobertura.xml --coverage-output-format cobertura
# Raw runner help when the repo needs direct TUnit app switches
dotnet run --project tests/MyProject.Tests/MyProject.Tests.csproj -- --help
Filter syntax: /<Assembly>/<Namespace>/<Class>/<Test> with * wildcards. See references/patterns.md for full examples.
Example Requests
- "Run this TUnit project correctly."
- "Fix our TUnit CI command."
- "Add a regression test in TUnit without breaking parallelism."
Files (dotnet-skills)
-
references
-
integration-testing.md 6.4 KB
# TUnit Integration Testing Patterns Use this reference when the repo uses TUnit for integration, API, SignalR, Orleans, or Playwright-driven UI suites rather than only pure unit tests. The patterns below are grounded in working suites from `AIBase` and `WA.Storied.Agents`: shared per-session fixtures, Aspire-backed distributed application boot, optional `WebApplicationFactory` layering for DI/grain access, and concrete artifact capture on failures. ## Pick The Right Fixture Level | Need | Recommended Pattern | |---|---| | Plain logic verification | normal TUnit test class with no shared infra | | Real HTTP/API/resource graph | `ClassDataSource<AspireTestFixture>(Shared = SharedType.PerTestSession)` | | Direct DI services, managers, or grains | `ClassDataSource<AIBaseTestApplication>(Shared = SharedType.PerTestSession)` or equivalent `WebApplicationFactory` wrapper | | Browser automation | shared Aspire/AppHost fixture plus Playwright helpers or `TUnit.Playwright` | ## Shared AppHost Fixture For real distributed tests, keep one AppHost boot per test session: ```csharp [ClassDataSource<AspireTestFixture>(Shared = SharedType.PerTestSession)] public sealed class HealthTests(AspireTestFixture fixture) { [Test] public async Task Health_endpoint_returns_ok() { using var client = fixture.CreateApiClient(); var response = await client.GetAsync("/health"); await Assert.That(response.IsSuccessStatusCode).IsTrue(); } } ``` This is the right shape for: - API health and contract tests - SignalR or SSE flows - Playwright-backed UI tests - AppHost resource graph and startup validation ## Mix TUnit With `WebApplicationFactory` When the test needs runtime services or Orleans grains from the Host container, keep the TUnit fixture but expose a `WebApplicationFactory` wrapper: ```csharp [ClassDataSource<TestApplication>(Shared = SharedType.PerTestSession)] public sealed class OrderRuntimeTests(TestApplication app) { [Test] public async Task Order_runtime_can_resolve_grain_from_host_scope() { await using var scope = app.CreateScope(); var grainFactory = scope.ServiceProvider.GetRequiredService<IGrainFactory>(); var grain = grainFactory.GetGrain<IOrderGrain>(Guid.NewGuid()); await grain.SubmitAsync(new SubmitOrder("PO-42")); var state = await grain.GetStateAsync(); await Assert.That(state.Number).IsEqualTo("PO-42"); } } ``` This pattern is especially useful for: - Orleans grain integration tests - service-manager and repository tests - host-level dependency graph validation - co-hosted SignalR/API/runtime tests ## TUnit Hooks For Deterministic Context Use hooks for per-test context that must be reset cleanly: ```csharp public abstract class IntegrationTestBase(TestApplication app) { protected TestApplication Application { get; } = app; private IDisposable? _scope; [Before(Test)] public void SetContext() { _scope = TestRequestContextScope.Push("test-user"); } [After(Test)] public void ClearContext() { _scope?.Dispose(); _scope = null; } } ``` Keep hooks small, explicit, and local to the test concern. ## Focused Commands For TUnit on Microsoft.Testing.Platform, keep the repo's command shape and pass framework switches after `--`: ```bash # Full project dotnet test --project Tests/MyProject.Tests/MyProject.Tests.csproj # One class dotnet test --project Tests/MyProject.Tests/MyProject.Tests.csproj -- --treenode-filter "/*/*/ChatControllerTests/*" # One category dotnet test --project Tests/MyProject.Tests/MyProject.Tests.csproj -- --treenode-filter "/*/*/*/*[Category=Integration]" # Coverage dotnet test --project Tests/MyProject.Tests/MyProject.Tests.csproj -- --coverage --coverage-output coverage.cobertura.xml --coverage-output-format cobertura ``` Do not use VSTest-style `--filter` for TUnit suites. Do not put TUnit switches before `--`. ## Playwright In TUnit Suites Shared fixture boot: ```csharp public async Task InitializePlaywrightAsync() { var exitCode = Microsoft.Playwright.Program.Main(["install", "chromium"]); if (exitCode != 0) { throw new InvalidOperationException($"Playwright install failed: {exitCode}"); } Playwright ??= await Microsoft.Playwright.Playwright.CreateAsync(); Browser ??= await Playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { Headless = true }); } ``` Per-test usage: ```csharp await fixture.InitializePlaywrightAsync(); await using var context = await fixture.CreateBrowserContextAsync(); var page = await context.NewPageAsync(); ``` Reuse the browser process, but never reuse a mutable page or browser context across tests. ## Capture Results And Failure Evidence Good TUnit integration suites capture more than just the assertion failure: - host-side error log dump on HTTP 500 or startup failures - coverage output such as `coverage.cobertura.xml` - screenshots and HTML for Playwright failures - narrowed console output from the fixture rather than unfiltered infrastructure noise Example failure-artifact pattern: ```csharp private static async Task CaptureArtifactsAsync(IPage page) { Directory.CreateDirectory("artifacts"); var timestamp = DateTime.UtcNow.ToString("yyyyMMddHHmmssfff", CultureInfo.InvariantCulture); await page.ScreenshotAsync(new PageScreenshotOptions { Path = Path.Combine("artifacts", $"failure-{timestamp}.png"), FullPage = true }); await File.WriteAllTextAsync( Path.Combine("artifacts", $"failure-{timestamp}.html"), await page.ContentAsync()); } ``` Example server-log dump pattern: ```csharp var logStart = DateTimeOffset.UtcNow; try { var response = await app.CreateApiClient().GetAsync("/health"); response.EnsureSuccessStatusCode(); } catch { Console.WriteLine(app.GetErrorLogDump(logStart)); throw; } ``` ## Practical Rules - Prefer `ClassDataSource<...>(Shared = SharedType.PerTestSession)` for expensive distributed fixtures. - Keep fixture code responsible for startup, teardown, and shared helpers only; assertions belong in the test classes. - Resolve real connection strings and endpoints from the Aspire fixture instead of copying appsettings into the test project. - Use coverage and filter switches that match Microsoft.Testing.Platform, not VSTest conventions. - Capture enough diagnostics on the first failing run so the next step is a fix, not a blind rerun. -
migration.md 12.4 KB
# Migrating to TUnit ## From xUnit ### Package Changes Remove: ```xml <PackageReference Include="xunit" Version="*" /> <PackageReference Include="xunit.runner.visualstudio" Version="*" /> ``` Add: ```xml <PackageReference Include="TUnit" Version="*" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="*" /> ``` ### Attribute Mappings | xUnit | TUnit | |-------|-------| | `[Fact]` | `[Test]` | | `[Theory]` | `[Test]` with data attributes | | `[InlineData(...)]` | `[Arguments(...)]` | | `[MemberData(...)]` | `[MethodDataSource(...)]` | | `[ClassData(...)]` | `[ClassDataSource<T>]` | | `[Trait("Category", "...")]` | `[Category("...")]` | | `[Collection("...")]` | `[NotInParallel("...")]` | ### Constructor Injection xUnit: ```csharp public class MyTests { private readonly ITestOutputHelper _output; public MyTests(ITestOutputHelper output) { _output = output; } [Fact] public void TestMethod() { _output.WriteLine("Test output"); Assert.True(true); } } ``` TUnit (with primary constructor): ```csharp public class MyTests(ITestOutputHelper output) { [Test] public async Task TestMethod() { output.WriteLine("Test output"); await Assert.That(true).IsTrue(); } } ``` ### Basic Test Conversion xUnit: ```csharp public class CalculatorTests { [Fact] public void Add_ReturnsSum() { var calc = new Calculator(); var result = calc.Add(2, 3); Assert.Equal(5, result); } [Theory] [InlineData(1, 1, 2)] [InlineData(2, 3, 5)] [InlineData(-1, 1, 0)] public void Add_WithParameters_ReturnsExpected(int a, int b, int expected) { var calc = new Calculator(); Assert.Equal(expected, calc.Add(a, b)); } } ``` TUnit: ```csharp public class CalculatorTests { [Test] public async Task Add_ReturnsSum() { var calc = new Calculator(); var result = calc.Add(2, 3); await Assert.That(result).IsEqualTo(5); } [Test] [Arguments(1, 1, 2)] [Arguments(2, 3, 5)] [Arguments(-1, 1, 0)] public async Task Add_WithParameters_ReturnsExpected(int a, int b, int expected) { var calc = new Calculator(); await Assert.That(calc.Add(a, b)).IsEqualTo(expected); } } ``` ### MemberData to MethodDataSource xUnit: ```csharp public class DataTests { public static IEnumerable<object[]> TestData => new List<object[]> { new object[] { "hello", 5 }, new object[] { "world", 5 } }; [Theory] [MemberData(nameof(TestData))] public void Test_WithMemberData(string input, int expectedLength) { Assert.Equal(expectedLength, input.Length); } } ``` TUnit: ```csharp public class DataTests { public static IEnumerable<(string, int)> TestData() { yield return ("hello", 5); yield return ("world", 5); } [Test] [MethodDataSource(nameof(TestData))] public async Task Test_WithMethodData(string input, int expectedLength) { await Assert.That(input.Length).IsEqualTo(expectedLength); } } ``` ### Lifecycle Hooks xUnit: ```csharp public class LifecycleTests : IDisposable, IAsyncLifetime { public async Task InitializeAsync() { // Before each test } public async Task DisposeAsync() { // After each test } public void Dispose() { // Cleanup } } ``` TUnit: ```csharp public class LifecycleTests : IAsyncDisposable { [Before(Test)] public async Task BeforeEachTest() { // Before each test } [After(Test)] public async Task AfterEachTest() { // After each test } public async ValueTask DisposeAsync() { // Cleanup } } ``` ### Collection Fixtures (Shared Context) xUnit: ```csharp [CollectionDefinition("Database")] public class DatabaseCollection : ICollectionFixture<DatabaseFixture> { } [Collection("Database")] public class DatabaseTests { private readonly DatabaseFixture _fixture; public DatabaseTests(DatabaseFixture fixture) { _fixture = fixture; } } ``` TUnit: ```csharp public class DatabaseTests { private static DatabaseFixture _fixture = null!; [Before(Class)] public static async Task SetupFixture() { _fixture = new DatabaseFixture(); await _fixture.InitializeAsync(); } [After(Class)] public static async Task TeardownFixture() { await _fixture.DisposeAsync(); } [Test] [NotInParallel("Database")] public async Task DatabaseTest() { // Use _fixture } } ``` ### Assertion Mappings | xUnit | TUnit | |-------|-------| | `Assert.Equal(expected, actual)` | `await Assert.That(actual).IsEqualTo(expected)` | | `Assert.NotEqual(unexpected, actual)` | `await Assert.That(actual).IsNotEqualTo(unexpected)` | | `Assert.True(condition)` | `await Assert.That(condition).IsTrue()` | | `Assert.False(condition)` | `await Assert.That(condition).IsFalse()` | | `Assert.Null(obj)` | `await Assert.That(obj).IsNull()` | | `Assert.NotNull(obj)` | `await Assert.That(obj).IsNotNull()` | | `Assert.Empty(collection)` | `await Assert.That(collection).IsEmpty()` | | `Assert.NotEmpty(collection)` | `await Assert.That(collection).IsNotEmpty()` | | `Assert.Contains(item, collection)` | `await Assert.That(collection).Contains(item)` | | `Assert.Throws<T>(action)` | `await Assert.That(action).ThrowsException().OfType<T>()` | | `await Assert.ThrowsAsync<T>(func)` | `await Assert.That(func).ThrowsException().OfType<T>()` | --- ## From NUnit ### Package Changes Remove: ```xml <PackageReference Include="NUnit" Version="*" /> <PackageReference Include="NUnit3TestAdapter" Version="*" /> ``` Add: ```xml <PackageReference Include="TUnit" Version="*" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="*" /> ``` ### Attribute Mappings | NUnit | TUnit | |-------|-------| | `[Test]` | `[Test]` | | `[TestCase(...)]` | `[Test]` with `[Arguments(...)]` | | `[TestCaseSource(...)]` | `[MethodDataSource(...)]` | | `[Category("...")]` | `[Category("...")]` | | `[SetUp]` | `[Before(Test)]` | | `[TearDown]` | `[After(Test)]` | | `[OneTimeSetUp]` | `[Before(Class)]` | | `[OneTimeTearDown]` | `[After(Class)]` | | `[Ignore("...")]` | `[Skip("...")]` | | `[Timeout(...)]` | `[Timeout(...)]` | | `[Retry(...)]` | `[Retry(...)]` | | `[Order(...)]` | `[DependsOn(...)]` | | `[NonParallelizable]` | `[NotInParallel]` | ### Basic Test Conversion NUnit: ```csharp [TestFixture] public class CalculatorTests { private Calculator _calculator; [SetUp] public void Setup() { _calculator = new Calculator(); } [Test] public void Add_ReturnsSum() { var result = _calculator.Add(2, 3); Assert.That(result, Is.EqualTo(5)); } [TestCase(1, 1, 2)] [TestCase(2, 3, 5)] [TestCase(-1, 1, 0)] public void Add_WithParameters_ReturnsExpected(int a, int b, int expected) { Assert.That(_calculator.Add(a, b), Is.EqualTo(expected)); } } ``` TUnit: ```csharp public class CalculatorTests { private Calculator _calculator = null!; [Before(Test)] public async Task Setup() { _calculator = new Calculator(); await Task.CompletedTask; } [Test] public async Task Add_ReturnsSum() { var result = _calculator.Add(2, 3); await Assert.That(result).IsEqualTo(5); } [Test] [Arguments(1, 1, 2)] [Arguments(2, 3, 5)] [Arguments(-1, 1, 0)] public async Task Add_WithParameters_ReturnsExpected(int a, int b, int expected) { await Assert.That(_calculator.Add(a, b)).IsEqualTo(expected); } } ``` ### TestCaseSource to MethodDataSource NUnit: ```csharp public class DataTests { private static IEnumerable<TestCaseData> TestCases() { yield return new TestCaseData("hello", 5).SetName("HelloCase"); yield return new TestCaseData("world", 5).SetName("WorldCase"); } [TestCaseSource(nameof(TestCases))] public void Test_WithTestCaseSource(string input, int expectedLength) { Assert.That(input.Length, Is.EqualTo(expectedLength)); } } ``` TUnit: ```csharp public class DataTests { public static IEnumerable<(string, int)> TestCases() { yield return ("hello", 5); yield return ("world", 5); } [Test] [MethodDataSource(nameof(TestCases))] public async Task Test_WithMethodData(string input, int expectedLength) { await Assert.That(input.Length).IsEqualTo(expectedLength); } } ``` ### OneTimeSetUp/OneTimeTearDown NUnit: ```csharp [TestFixture] public class DatabaseTests { private static Database _db; [OneTimeSetUp] public void OneTimeSetup() { _db = new Database(); _db.Connect(); } [OneTimeTearDown] public void OneTimeTeardown() { _db.Disconnect(); } [Test] public void QueryTest() { var result = _db.Query("SELECT 1"); Assert.That(result, Is.Not.Null); } } ``` TUnit: ```csharp public class DatabaseTests { private static Database _db = null!; [Before(Class)] public static async Task OneTimeSetup() { _db = new Database(); await _db.ConnectAsync(); } [After(Class)] public static async Task OneTimeTeardown() { await _db.DisconnectAsync(); } [Test] public async Task QueryTest() { var result = await _db.QueryAsync("SELECT 1"); await Assert.That(result).IsNotNull(); } } ``` ### Constraint-Based Assertions | NUnit | TUnit | |-------|-------| | `Assert.That(x, Is.EqualTo(y))` | `await Assert.That(x).IsEqualTo(y)` | | `Assert.That(x, Is.Not.EqualTo(y))` | `await Assert.That(x).IsNotEqualTo(y)` | | `Assert.That(x, Is.Null)` | `await Assert.That(x).IsNull()` | | `Assert.That(x, Is.Not.Null)` | `await Assert.That(x).IsNotNull()` | | `Assert.That(x, Is.True)` | `await Assert.That(x).IsTrue()` | | `Assert.That(x, Is.False)` | `await Assert.That(x).IsFalse()` | | `Assert.That(x, Is.GreaterThan(y))` | `await Assert.That(x).IsGreaterThan(y)` | | `Assert.That(x, Is.LessThan(y))` | `await Assert.That(x).IsLessThan(y)` | | `Assert.That(list, Has.Count.EqualTo(n))` | `await Assert.That(list).HasCount(n)` | | `Assert.That(list, Contains.Item(x))` | `await Assert.That(list).Contains(x)` | | `Assert.That(list, Is.Empty)` | `await Assert.That(list).IsEmpty()` | | `Assert.That(s, Does.StartWith("x"))` | `await Assert.That(s).StartsWith("x")` | | `Assert.That(s, Does.Contain("x"))` | `await Assert.That(s).Contains("x")` | | `Assert.That(() => x, Throws.TypeOf<T>())` | `await Assert.That(() => x).ThrowsException().OfType<T>()` | | `Assert.Multiple(() => { ... })` | `await Assert.Multiple(() => { ... })` | ### Parallelism Control NUnit: ```csharp [TestFixture] [NonParallelizable] public class SequentialTests { [Test] public void Test1() { } [Test] public void Test2() { } } [TestFixture] [Parallelizable(ParallelScope.None)] public class AnotherSequentialTests { } ``` TUnit: ```csharp [NotInParallel] public class SequentialTests { [Test] public async Task Test1() { } [Test] public async Task Test2() { } } // Or at the test level: public class MixedParallelTests { [Test] [NotInParallel] public async Task SequentialTest() { } [Test] public async Task ParallelTest() { } } ``` --- ## Common Migration Steps 1. **Update packages** in the project file 2. **Replace attributes** using the mapping tables above 3. **Convert assertions** to async TUnit assertions 4. **Update lifecycle hooks** from interfaces to attributes 5. **Add async/await** to test methods (TUnit assertions are async) 6. **Review parallelism** - TUnit is parallel by default 7. **Run tests** and fix any remaining compilation errors ### Automated Find-Replace Patterns ``` // xUnit [Fact] -> [Test] [Theory] -> [Test] [InlineData( -> [Arguments( Assert.Equal( -> await Assert.That( Assert.True( -> await Assert.That( Assert.NotNull( -> await Assert.That( // NUnit [TestCase( -> [Arguments( [SetUp] -> [Before(Test)] [TearDown] -> [After(Test)] [OneTimeSetUp] -> [Before(Class)] [OneTimeTearDown] -> [After(Class)] Assert.That(x, Is.EqualTo(y)) -> await Assert.That(x).IsEqualTo(y) ``` ### Post-Migration Checklist - [ ] All tests compile - [ ] All tests pass with `dotnet test` - [ ] Parallel execution works correctly - [ ] Shared state is properly isolated - [ ] CI pipeline updated if needed - [ ] TUnit analyzers enabled and warnings addressed -
patterns.md 12.5 KB
# TUnit Patterns ## Source Generation TUnit uses source generators to discover and wire up tests at compile time rather than runtime reflection. This means: - Tests are compiled as static method invocations - Build errors surface test discovery issues early - No runtime reflection overhead - IDE support for test discovery depends on generator output ### Basic Test Structure ```csharp public class CalculatorTests(ITestOutputHelper output) { [Test] public async Task Add_ReturnsCorrectSum() { var calculator = new Calculator(); var result = calculator.Add(2, 3); await Assert.That(result).IsEqualTo(5); } } ``` ### Parameterized Tests with Arguments ```csharp public class MathTests { [Test] [Arguments(1, 2, 3)] [Arguments(0, 0, 0)] [Arguments(-1, 1, 0)] public async Task Add_WithVariousInputs_ReturnsExpectedSum(int a, int b, int expected) { var result = Calculator.Add(a, b); await Assert.That(result).IsEqualTo(expected); } } ``` ### Matrix Tests Combine multiple argument sources to create test matrices: ```csharp public class MatrixTests { [Test] [MatrixDataSource] public async Task ProcessData_HandlesAllCombinations( [Matrix("json", "xml", "csv")] string format, [Matrix(true, false)] bool compress) { var processor = new DataProcessor(); var result = await processor.ProcessAsync(format, compress); await Assert.That(result.Success).IsTrue(); } } ``` ### Method Data Source ```csharp public class DataDrivenTests { [Test] [MethodDataSource(nameof(GetTestCases))] public async Task Validate_WithMethodData(string input, bool expected) { var result = Validator.IsValid(input); await Assert.That(result).IsEqualTo(expected); } public static IEnumerable<(string, bool)> GetTestCases() { yield return ("valid@email.com", true); yield return ("invalid", false); yield return ("", false); } } ``` ### Class Data Source ```csharp public sealed class SharedFixture : IAsyncInitializer, IAsyncDisposable { public Task InitializeAsync() => Task.CompletedTask; public ValueTask DisposeAsync() => ValueTask.CompletedTask; } public class ClassDataTests { [Test] [ClassDataSource<SharedFixture>(Shared = SharedType.PerTestSession)] public async Task Process_WithSharedFixture(SharedFixture fixture) { await Assert.That(fixture).IsNotNull(); } } ``` Use `Shared = SharedType.PerTestSession` for expensive integration fixtures such as AppHost boot, `WebApplicationFactory`, or browser setup. For full distributed-app patterns, load `integration-testing.md`. ## Parallel Testing TUnit runs tests in parallel by default. Design tests for isolation. ### Default Parallel Execution All tests run in parallel unless explicitly constrained: ```csharp public class ParallelTests { [Test] public async Task Test1() => await Task.Delay(100); [Test] public async Task Test2() => await Task.Delay(100); // Both tests run simultaneously } ``` ### Controlling Parallelism Disable parallelism for a specific test: ```csharp public class SharedResourceTests { [Test] [NotInParallel] public async Task Test_ThatModifiesGlobalState() { // Runs alone, not in parallel with other [NotInParallel] tests } } ``` Group tests that must not run together: ```csharp public class DatabaseTests { [Test] [NotInParallel("Database")] public async Task CreateUser_InsertsRecord() { // Other tests with [NotInParallel("Database")] wait } [Test] [NotInParallel("Database")] public async Task DeleteUser_RemovesRecord() { // Runs sequentially with other "Database" group tests } } ``` ### Parallel Limits Limit concurrent test execution: ```csharp [assembly: ParallelLimiter<MaxParallelTests>] public class MaxParallelTests : IParallelLimit { public int Limit => Environment.ProcessorCount; } ``` ### Class-Level Parallelism Control ```csharp [NotInParallel] public class SequentialTestClass { [Test] public async Task Test1() { } [Test] public async Task Test2() { } // All tests in this class run sequentially } ``` ## Assertions TUnit provides fluent, async-first assertions. ### Basic Assertions ```csharp [Test] public async Task BasicAssertions() { var value = 42; var text = "Hello"; var list = new[] { 1, 2, 3 }; await Assert.That(value).IsEqualTo(42); await Assert.That(value).IsGreaterThan(40); await Assert.That(value).IsLessThanOrEqualTo(42); await Assert.That(text).IsNotNull(); await Assert.That(text).IsNotEmpty(); await Assert.That(text).Contains("ell"); await Assert.That(text).StartsWith("He"); await Assert.That(list).HasCount(3); await Assert.That(list).Contains(2); } ``` ### Exception Assertions ```csharp [Test] public async Task ThrowsException() { var action = () => throw new InvalidOperationException("test"); await Assert.That(action).ThrowsException() .OfType<InvalidOperationException>() .WithMessage("test"); } [Test] public async Task ThrowsAsync() { var asyncAction = async () => { await Task.Delay(1); throw new ArgumentNullException("param"); }; await Assert.That(asyncAction).ThrowsException() .OfType<ArgumentNullException>(); } ``` ### Collection Assertions ```csharp [Test] public async Task CollectionAssertions() { var items = new[] { 1, 2, 3, 4, 5 }; await Assert.That(items).HasCount(5); await Assert.That(items).Contains(3); await Assert.That(items).DoesNotContain(6); await Assert.That(items).AllSatisfy(x => x > 0); await Assert.That(items).IsEquivalentTo([5, 4, 3, 2, 1]); await Assert.That(items).IsInAscendingOrder(); } ``` ### Object Assertions ```csharp [Test] public async Task ObjectAssertions() { var user = new User("John", 30); await Assert.That(user).IsNotNull(); await Assert.That(user.Name).IsEqualTo("John"); await Assert.That(user.Age).IsGreaterThanOrEqualTo(18); await Assert.That(user).IsOfType<User>(); } ``` ### Multiple Assertions Group related assertions to report all failures: ```csharp [Test] public async Task MultipleAssertions() { var result = new Result(true, "OK", 200); await Assert.Multiple(() => { Assert.That(result.Success).IsTrue(); Assert.That(result.Message).IsEqualTo("OK"); Assert.That(result.Code).IsEqualTo(200); }); } ``` ## Test Lifecycle Hooks ### Setup and Teardown ```csharp public class LifecycleTests : IAsyncDisposable { private HttpClient _client = null!; [Before(Test)] public async Task BeforeEachTest() { _client = new HttpClient(); await Task.CompletedTask; } [After(Test)] public async Task AfterEachTest() { _client.Dispose(); await Task.CompletedTask; } [Test] public async Task TestWithHttpClient() { var response = await _client.GetAsync("https://example.com"); await Assert.That(response.IsSuccessStatusCode).IsTrue(); } public async ValueTask DisposeAsync() { _client.Dispose(); await Task.CompletedTask; } } ``` ### Class-Level Hooks ```csharp public class ClassLevelHooks { private static TestServer _server = null!; [Before(Class)] public static async Task BeforeAllTests() { _server = new TestServer(); await _server.StartAsync(); } [After(Class)] public static async Task AfterAllTests() { await _server.StopAsync(); } [Test] public async Task Test1() { } [Test] public async Task Test2() { } } ``` ### Assembly-Level Hooks ```csharp public class GlobalSetup { [Before(Assembly)] public static async Task GlobalBeforeAll() { await Database.MigrateAsync(); } [After(Assembly)] public static async Task GlobalAfterAll() { await Database.CleanupAsync(); } } ``` ## Dependency Injection ### Constructor Injection with Primary Constructors ```csharp public class ServiceTests( ITestOutputHelper output, CancellationToken cancellationToken) { [Test] public async Task Service_DoesWork() { output.WriteLine("Starting test"); var service = new MyService(); await service.DoWorkAsync(cancellationToken); await Assert.That(service.IsComplete).IsTrue(); } } ``` ### Using Test Context ```csharp public class ContextTests(TestContext context) { [Test] public async Task AccessTestMetadata() { var testName = context.TestDetails.TestName; var className = context.TestDetails.TestClass.Name; await Assert.That(testName).IsNotEmpty(); } } ``` ## Test Dependencies ### Explicit Test Ordering ```csharp public class OrderedTests { private static int _counter; [Test] [DependsOn(nameof(First))] public async Task Second() { await Assert.That(_counter).IsEqualTo(1); _counter++; } [Test] public async Task First() { _counter = 1; await Task.CompletedTask; } [Test] [DependsOn(nameof(Second))] public async Task Third() { await Assert.That(_counter).IsEqualTo(2); } } ``` ## Timeouts ### Test-Level Timeout ```csharp public class TimeoutTests { [Test] [Timeout(5000)] // 5 seconds public async Task MustCompleteQuickly() { await Task.Delay(100); await Assert.That(true).IsTrue(); } } ``` ### Class-Level Timeout ```csharp [Timeout(10000)] public class TimeBoundTests { [Test] public async Task Test1() { } [Test] public async Task Test2() { } } ``` ## Categories and Filtering ### Test Categories ```csharp public class CategorizedTests { [Test] [Category("Unit")] public async Task UnitTest() { } [Test] [Category("Integration")] public async Task IntegrationTest() { } [Test] [Category("Unit")] [Category("Fast")] public async Task FastUnitTest() { } } ``` ### Skipping Tests ```csharp public class SkipTests { [Test] [Skip("Pending implementation")] public async Task NotYetImplemented() { } [Test] [SkipWhen(nameof(ShouldSkip))] public async Task ConditionallySkipped() { } public static bool ShouldSkip() => !Environment.GetEnvironmentVariable("RUN_SLOW_TESTS")?.Equals("true", StringComparison.OrdinalIgnoreCase) ?? true; } ``` ## Custom Attributes ### Retry Failed Tests ```csharp public class RetryTests { [Test] [Retry(3)] public async Task FlakyTest() { // Will retry up to 3 times on failure var result = await ExternalService.CallAsync(); await Assert.That(result.Success).IsTrue(); } } ``` ### Repeat Tests ```csharp public class RepeatTests { [Test] [Repeat(5)] public async Task RunMultipleTimes() { // Runs 5 times to check for intermittent issues await Assert.That(true).IsTrue(); } } ``` ## Running Tests TUnit uses Microsoft.Testing.Platform. Prefer `dotnet run` over `dotnet test` for full CLI flag access. ### Basic Execution ```bash # Run via test host (recommended) dotnet run --project Tests.csproj # Run via dotnet test dotnet test Tests.csproj ``` ### Filtering with --treenode-filter TUnit uses `--treenode-filter`, not `--filter`. Syntax: `/<Assembly>/<Namespace>/<Class>/<Test>` ```bash # All tests in a class dotnet run --project Tests.csproj -- --treenode-filter "/*/*/CalculatorTests/*" # Specific test method dotnet run --project Tests.csproj -- --treenode-filter "/*/*/CalculatorTests/Add_ReturnsSum" # Filter by namespace dotnet run --project Tests.csproj -- --treenode-filter "/*/MyApp.Tests.Unit/*/*" # Filter by category dotnet run --project Tests.csproj -- --treenode-filter "/*/*/*/*[Category=Unit]" # Exclude category dotnet run --project Tests.csproj -- --treenode-filter "/*/*/*/*[Category!=Slow]" # Multiple filters (OR) dotnet run --project Tests.csproj -- --treenode-filter "/*/*/ClassA/*|/*/*/ClassB/*" # Combine filters (AND) dotnet run --project Tests.csproj -- --treenode-filter "/*/*/*/*[Category=Unit][Priority=High]" # Custom property filter dotnet run --project Tests.csproj -- --treenode-filter "/*/*/*/*[Owner=TeamA]" ``` ### Other CLI Options ```bash # List available tests dotnet run --project Tests.csproj -- --list-tests # Run with specific timeout dotnet run --project Tests.csproj -- --timeout 60000 # Output detailed results dotnet run --project Tests.csproj -- --results-directory ./results ``` -
tunit.md 2.7 KB
# TUnit in MCAF ## Open/Free Status - open source - free to use ## Install Template-based start: ```bash dotnet new install TUnit.Templates dotnet new TUnit -n MyTestProject ``` ## Verify First Before adding packages or templates, check whether the repo already uses TUnit: ```bash rg -n "PackageReference Include=\"TUnit\"|\\[Test\\]|\\[Arguments\\]|ParallelLimiter|DependsOn" -g '*.csproj' -g '*.cs' . ``` Use this reference when the repository already chose TUnit and you need the right commands, expectations, and CI integration points. For shared AppHost fixtures, `WebApplicationFactory`, Playwright UI harnesses, and log/artifact capture, load `integration-testing.md`. ## Detect TUnit Look for the package and its common attributes: ```bash rg -n "PackageReference Include=\"TUnit\"|\\[Test\\]|\\[Arguments\\]|ParallelLimiter|DependsOn" -g '*.csproj' -g '*.cs' . ``` TUnit is built on Microsoft.Testing.Platform, uses source generation for test discovery, and runs tests in parallel by default. ## Common Commands Start with the repo's `test` command from `AGENTS.md`. If the repo has not documented one yet, these are the safe defaults: ```bash dotnet test MySolution.sln dotnet test tests/MyProject.Tests/MyProject.Tests.csproj dotnet test tests/MyProject.Tests/MyProject.Tests.csproj -- --treenode-filter "/*/*/MyTestClass/*" dotnet test MySolution.sln -- --coverage --coverage-output coverage.cobertura.xml --coverage-output-format cobertura dotnet run --project tests/MyProject.Tests/MyProject.Tests.csproj -- --help ``` New-project quick start from the official template: ```bash cd MyTestProject dotnet run ``` ## CI Notes - Assume concurrency unless the repo has explicitly limited it. - Do not share mutable static state, temp paths, or fixed ports across tests. - Build once, then re-run focused projects with `--no-build` where the repo supports it. - For coverage on Microsoft.Testing.Platform, prefer the repo's documented MTP coverage switches such as `--coverage --coverage-output ...`. - Publish human-readable reports separately with ReportGenerator if the pipeline needs HTML or Markdown summaries. - For integration/UI suites, capture first-failure evidence: host log dumps, screenshots, and HTML artifacts. ## Good Defaults - use `[Arguments]` for stable parameterized cases - use `[DependsOn]` sparingly and only for real orchestration constraints - use `[ParallelLimiter]` locally when a boundary genuinely cannot run at full concurrency - keep hooks small and explicit so parallel failures stay diagnosable ## Sources - [TUnit GitHub repository](https://github.com/thomhurst/TUnit) - [Microsoft.Testing.Platform overview](https://learn.microsoft.com/en-us/dotnet/core/testing/microsoft-testing-platform-intro)
-
-
SKILL.md 7.5 KB
--- name: dotnet-tunit version: "1.1.0" category: "Testing" description: "Write, run, or repair .NET tests that use TUnit. Use when a repo uses `TUnit`, `TUnit.Playwright`, `[Test]`, `[Arguments]`, `ClassDataSource`, `SharedType.PerTestSession`, or Microsoft.Testing.Platform-based execution." compatibility: "Requires a .NET solution or project with TUnit packages; respects the repo's `AGENTS.md` commands first." --- # TUnit ## Trigger On - the repo uses TUnit - you need to add, run, debug, or repair TUnit tests - the repo uses Microsoft.Testing.Platform-based test execution - the repo uses `ClassDataSource<...>(Shared = SharedType.PerTestSession)`, `ParallelLimiter`, `TUnit.Playwright`, or `--treenode-filter` ## Value - produce a concrete project delta: code, docs, config, tests, CI, or review artifact - reduce ambiguity through explicit planning, verification, and final validation skills - leave reusable project context so future tasks are faster and safer ## Do Not Use For - xUnit projects - MSTest projects - generic test strategy with no TUnit-specific mechanics ## Inputs - the nearest `AGENTS.md` - the test project file and package references - the repo's current TUnit execution command ## Quick Start 1. Read the nearest `AGENTS.md` and confirm scope and constraints. 2. Run this skill's `Workflow` through the `Ralph Loop` until outcomes are acceptable. 3. Return the `Required Result Format` with concrete artifacts and verification evidence. ## Workflow 1. Confirm the project really uses TUnit and not a different MTP-based framework. 2. Read the repo's real `test` command from `AGENTS.md`. If the repo has no explicit command yet, start with `dotnet test PROJECT_OR_SOLUTION`. 3. Keep the TUnit execution model intact: - tests are source-generated at build time - tests run in parallel by default - built-in analyzers should remain enabled 4. Choose the fixture level deliberately: - plain TUnit tests for isolated logic - shared AppHost/Aspire fixtures for HTTP, SignalR, SSE, or UI flows - `WebApplicationFactory` layered over shared Aspire infra when tests need Host DI services, `IGrainFactory`, or other runtime internals 5. Reuse expensive fixtures with `ClassDataSource<Fixture>(Shared = SharedType.PerTestSession)` instead of booting distributed infrastructure per test. 6. Fix isolation bugs instead of globally serializing the suite unless the repo already documented a justified exception. 7. Run the narrowest useful scope first with `dotnet test ... -- --treenode-filter "..."`. Keep TUnit arguments after `--`. 8. Capture useful failure evidence: host log dumps, focused console output, coverage files, and Playwright screenshots/HTML for UI tests. 9. Use `[Test]`, `[Arguments]`, hooks, and dependencies only when they make the scenario clearer, not because the framework allows it. ## Bootstrap When Missing If `TUnit` is requested but not configured yet: 1. Detect current state: - `rg -n "TUnit|Microsoft\\.Testing\\.Platform" -g '*.csproj' -g 'Directory.Build.*' .` 2. Add the minimal package set to the test project: - `dotnet add TEST_PROJECT.csproj package TUnit` - add `Microsoft.NET.Test.Sdk` only when the repo's chosen TUnit project shape requires it; do not blindly duplicate runner packages 3. Keep the runner model explicit in `AGENTS.md` and CI: - record that the repo uses Microsoft.Testing.Platform-compatible execution for this test project - record the exact `dotnet test TEST_PROJECT.csproj` command the repo will use 4. Add one small executable test using `[Test]`. 5. Run `dotnet test TEST_PROJECT.csproj` and return `status: configured` or `status: improved`. 6. If the repo intentionally standardizes on xUnit or MSTest, return `status: not_applicable` unless migration is explicitly requested. ## Deliver - TUnit tests that respect source generation and parallel execution - commands that work in local and CI runs - framework-specific verification guidance for the repo - a fixture strategy that matches the actual test scope: logic-only, AppHost/API, Host DI/grains, or Playwright UI ## Validate - the command matches the repo's TUnit runner style - focused runs use `--treenode-filter` rather than VSTest-style `--filter` - shared distributed fixtures use `SharedType.PerTestSession` or an equivalent reuse pattern - shared state is isolated or explicitly controlled - built-in TUnit analyzers remain active - coverage tooling matches Microsoft.Testing.Platform if coverage is enabled - UI failures capture artifacts and server-side failures expose enough logs to avoid blind reruns ## Test Harness ```mermaid flowchart LR A["TUnit task"] --> B{"What does the test need?"} B -->|"Single component only"| C["Plain TUnit test"] B -->|"HTTP / SignalR / resource graph"| D["Shared Aspire/AppHost fixture"] B -->|"Host DI / grains / runtime services"| E["Shared Aspire/AppHost fixture + WebApplicationFactory"] B -->|"Browser automation"| F["Shared Aspire/AppHost fixture + Playwright"] C & D & E & F --> G["Run focused with --treenode-filter"] G --> H["Capture logs, artifacts, and coverage"] ``` ## Ralph Loop Use the Ralph Loop for every task, including docs, architecture, testing, and tooling work. 1. Plan first (mandatory): - analyze current state - define target outcome, constraints, and risks - write a detailed execution plan - list final validation skills to run at the end, with order and reason 2. Execute one planned step and produce a concrete delta. 3. Review the result and capture findings with actionable next fixes. 4. Apply fixes in small batches and rerun the relevant checks or review steps. 5. Update the plan after each iteration. 6. Repeat until outcomes are acceptable or only explicit exceptions remain. 7. If a dependency is missing, bootstrap it or return `status: not_applicable` with explicit reason and fallback path. ### Required Result Format - `status`: `complete` | `clean` | `improved` | `configured` | `not_applicable` | `blocked` - `plan`: concise plan and current iteration step - `actions_taken`: concrete changes made - `validation_skills`: final skills run, or skipped with reasons - `verification`: commands, checks, or review evidence summary - `remaining`: top unresolved items or `none` For setup-only requests with no execution, return `status: configured` and exact next commands. ## Load References - `references/patterns.md` - `references/migration.md` - `references/tunit.md` - `references/integration-testing.md` ## Running Tests TUnit uses Microsoft.Testing.Platform. Use `--treenode-filter` for filtering (not `--filter`), and keep runner switches after `--`. ```bash # Run all tests dotnet test MySolution.sln # Run one test project dotnet test tests/MyProject.Tests/MyProject.Tests.csproj # Filter by class dotnet test tests/MyProject.Tests/MyProject.Tests.csproj -- --treenode-filter "/*/*/CalculatorTests/*" # Filter by category dotnet test tests/MyProject.Tests/MyProject.Tests.csproj -- --treenode-filter "/*/*/*/*[Category=Integration]" # Coverage on Microsoft.Testing.Platform dotnet test MySolution.sln -- --coverage --coverage-output coverage.cobertura.xml --coverage-output-format cobertura # Raw runner help when the repo needs direct TUnit app switches dotnet run --project tests/MyProject.Tests/MyProject.Tests.csproj -- --help ``` Filter syntax: `/<Assembly>/<Namespace>/<Class>/<Test>` with `*` wildcards. See `references/patterns.md` for full examples. ## Example Requests - "Run this TUnit project correctly." - "Fix our TUnit CI command." - "Add a regression test in TUnit without breaking parallelism."
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.