From c7b4a41c395e8eb60c3b235a42840982b5206c5d Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Tue, 19 May 2026 20:09:20 +0100
Subject: [PATCH 1/4] Update target framework to net10.0 and upgrade TUnit
package to version 1.45.8
---
.../run-tests/fixtures/mtp-tunit/TestProject.csproj | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/tests/dotnet-test/run-tests/fixtures/mtp-tunit/TestProject.csproj b/tests/dotnet-test/run-tests/fixtures/mtp-tunit/TestProject.csproj
index 29693a048d..785fd4b979 100644
--- a/tests/dotnet-test/run-tests/fixtures/mtp-tunit/TestProject.csproj
+++ b/tests/dotnet-test/run-tests/fixtures/mtp-tunit/TestProject.csproj
@@ -1,13 +1,13 @@
- net9.0
+ net10.0
Exe
false
-
+
From 087bab90f830fe80300df7491295e371de5a36d3 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Tue, 19 May 2026 20:21:18 +0100
Subject: [PATCH 2/4] Fix and expand TUnit coverage in dotnet-test-frameworks
reference
Cross-referenced against tunit.dev/llms.txt and the official attributes
comparison page. Corrects the test class marker (TUnit is convention-based,
not [ClassDataSource]), and fills in the previously-missing TUnit rows for
the assertion APIs, setup/teardown methods, exception-handling examples,
and integration test category marker.
---
.../skills/dotnet-test-frameworks/SKILL.md | 48 +++++++++++++------
1 file changed, 34 insertions(+), 14 deletions(-)
diff --git a/plugins/dotnet-test/skills/dotnet-test-frameworks/SKILL.md b/plugins/dotnet-test/skills/dotnet-test-frameworks/SKILL.md
index cfde1b7dce..dd6d53c301 100644
--- a/plugins/dotnet-test/skills/dotnet-test-frameworks/SKILL.md
+++ b/plugins/dotnet-test/skills/dotnet-test-frameworks/SKILL.md
@@ -16,23 +16,25 @@ Language-specific detection patterns for .NET test frameworks (MSTest, xUnit, NU
| MSTest | `[TestClass]` | `[TestMethod]`, `[DataTestMethod]` |
| xUnit | *(none — convention-based)* | `[Fact]`, `[Theory]` |
| NUnit | `[TestFixture]` | `[Test]`, `[TestCase]`, `[TestCaseSource]` |
-| TUnit | `[ClassDataSource]` | `[Test]` |
+| TUnit | *(none — convention-based)* | `[Test]` |
## Assertion APIs by Framework
-| Category | MSTest | xUnit | NUnit |
-| -------- | ------ | ----- | ----- |
-| Equality | `Assert.AreEqual` | `Assert.Equal` | `Assert.That(x, Is.EqualTo(y))` |
-| Boolean | `Assert.IsTrue` / `Assert.IsFalse` | `Assert.True` / `Assert.False` | `Assert.That(x, Is.True)` |
-| Null | `Assert.IsNull` / `Assert.IsNotNull` | `Assert.Null` / `Assert.NotNull` | `Assert.That(x, Is.Null)` |
-| Exception | `Assert.Throws()` / `Assert.ThrowsExactly()` | `Assert.Throws()` | `Assert.That(() => ..., Throws.TypeOf())` |
-| Collection | `CollectionAssert.Contains` | `Assert.Contains` | `Assert.That(col, Has.Member(x))` |
-| String | `StringAssert.Contains` | `Assert.Contains(str, sub)` | `Assert.That(str, Does.Contain(sub))` |
-| Type | `Assert.IsInstanceOfType` | `Assert.IsAssignableFrom` | `Assert.That(x, Is.InstanceOf())` |
-| Inconclusive | `Assert.Inconclusive()` | *skip via `[Fact(Skip)]`* | `Assert.Inconclusive()` |
-| Fail | `Assert.Fail()` | `Assert.Fail()` (.NET 10+) | `Assert.Fail()` |
+| Category | MSTest | xUnit | NUnit | TUnit |
+| -------- | ------ | ----- | ----- | ----- |
+| Equality | `Assert.AreEqual` | `Assert.Equal` | `Assert.That(x, Is.EqualTo(y))` | `await Assert.That(x).IsEqualTo(y)` |
+| Boolean | `Assert.IsTrue` / `Assert.IsFalse` | `Assert.True` / `Assert.False` | `Assert.That(x, Is.True)` | `await Assert.That(x).IsTrue()` / `.IsFalse()` |
+| Null | `Assert.IsNull` / `Assert.IsNotNull` | `Assert.Null` / `Assert.NotNull` | `Assert.That(x, Is.Null)` | `await Assert.That(x).IsNull()` / `.IsNotNull()` |
+| Exception | `Assert.Throws()` / `Assert.ThrowsExactly()` | `Assert.Throws()` | `Assert.That(() => ..., Throws.TypeOf())` | `await Assert.That(() => ...).Throws()` / `.ThrowsExactly()` |
+| Collection | `CollectionAssert.Contains` | `Assert.Contains` | `Assert.That(col, Has.Member(x))` | `await Assert.That(col).Contains(x)` |
+| String | `StringAssert.Contains` | `Assert.Contains(str, sub)` | `Assert.That(str, Does.Contain(sub))` | `await Assert.That(str).Contains(sub)` |
+| Type | `Assert.IsInstanceOfType` | `Assert.IsAssignableFrom` | `Assert.That(x, Is.InstanceOf())` | `await Assert.That(x).IsTypeOf()` |
+| Inconclusive | `Assert.Inconclusive()` | *skip via `[Fact(Skip)]`* | `Assert.Inconclusive()` | `Skip.Test("reason")` (no true inconclusive state) |
+| Fail | `Assert.Fail()` | `Assert.Fail()` (.NET 10+) | `Assert.Fail()` | `Assert.Fail()` |
-Third-party assertion libraries: `Should*` (Shouldly), `.Should()` (FluentAssertions / AwesomeAssertions), `Verify()` (Verify).
+**TUnit-specific:** assertions are async and **must be awaited** — a forgotten `await` causes the assertion to never run, and the test passes silently. A built-in analyzer warns when `await` is missing. Multiple assertions can be combined with `.And` / `.Or` chaining or grouped via `Assert.Multiple()`.
+
+Third-party assertion libraries: `Should*` (Shouldly), `.Should()` (FluentAssertions / AwesomeAssertions), `Verify()` (Verify). TUnit also ships an optional `TUnit.Assertions.Should` package providing FluentAssertions-style `value.Should().BeEqualTo(...)` on top of the same infrastructure.
## Sleep/Delay Patterns
@@ -86,6 +88,18 @@ var ex = Assert.Throws(
Assert.That(ex.Message, Is.EqualTo("Order must contain at least one item"));
```
+**TUnit:**
+
+```csharp
+await Assert.That(() => processor.ProcessOrder(emptyOrder))
+ .Throws()
+ .WithMessage("Order must contain at least one item");
+
+// Or, for exact-type matching (no derived types):
+await Assert.That(() => processor.ProcessOrder(emptyOrder))
+ .ThrowsExactly();
+```
+
## Mystery Guest — Common .NET Patterns
| Smell indicator | What to look for |
@@ -103,7 +117,7 @@ Recognize these as integration tests (adjust smell severity accordingly):
- Class name contains `Integration`, `E2E`, `EndToEnd`, or `Acceptance`
- `[TestCategory("Integration")]` (MSTest)
- `[Trait("Category", "Integration")]` (xUnit)
-- `[Category("Integration")]` (NUnit)
+- `[Category("Integration")]` (NUnit, TUnit)
- Project name ending in `.IntegrationTests` or `.E2ETests`
## Setup/Teardown Methods
@@ -113,6 +127,12 @@ Recognize these as integration tests (adjust smell severity accordingly):
| MSTest | `[TestInitialize]` or constructor | `[TestCleanup]` or `IDisposable.Dispose` / `IAsyncDisposable.DisposeAsync` |
| xUnit | constructor | `IDisposable.Dispose` / `IAsyncDisposable.DisposeAsync` |
| NUnit | `[SetUp]` | `[TearDown]` |
+| TUnit | `[Before(Test)]` or constructor | `[After(Test)]` or `IDisposable.Dispose` / `IAsyncDisposable.DisposeAsync` |
| MSTest (class) | `[ClassInitialize]` | `[ClassCleanup]` |
| NUnit (class) | `[OneTimeSetUp]` | `[OneTimeTearDown]` |
| xUnit (class) | `IClassFixture` | fixture's `Dispose` |
+| TUnit (class) | `[Before(Class)]` | `[After(Class)]` |
+| TUnit (assembly) | `[Before(Assembly)]` | `[After(Assembly)]` |
+| TUnit (session) | `[Before(TestSession)]` | `[After(TestSession)]` |
+
+**TUnit-specific:** `[BeforeEvery(Test)]` / `[AfterEvery(Test)]` (and the `Class` / `Assembly` variants) run for every test/class/assembly across the whole test run — useful for global cross-cutting hooks. Hooks may optionally accept a context object (`TestContext`, `ClassHookContext`, etc.) and/or a `CancellationToken`.
From 4655fb50ab3362ee01e33f42d1ede733d9ddf95e Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Tue, 19 May 2026 20:23:28 +0100
Subject: [PATCH 3/4] Refine TUnit assertion table accuracy
The Type row's TUnit cell now uses IsAssignableTo() to match the
assignable-type semantics of the MSTest/xUnit/NUnit equivalents, with a
note pointing at IsTypeOf() for exact-type checks. The Skip row now
documents that [Skip] also applies at class and assembly scope and that
Skip.Test("reason") handles dynamic in-test skipping.
Verified against the TUnit migration guides and the type-assertions docs.
---
plugins/dotnet-test/skills/dotnet-test-frameworks/SKILL.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/plugins/dotnet-test/skills/dotnet-test-frameworks/SKILL.md b/plugins/dotnet-test/skills/dotnet-test-frameworks/SKILL.md
index dd6d53c301..3a840502ab 100644
--- a/plugins/dotnet-test/skills/dotnet-test-frameworks/SKILL.md
+++ b/plugins/dotnet-test/skills/dotnet-test-frameworks/SKILL.md
@@ -28,7 +28,7 @@ Language-specific detection patterns for .NET test frameworks (MSTest, xUnit, NU
| Exception | `Assert.Throws()` / `Assert.ThrowsExactly()` | `Assert.Throws()` | `Assert.That(() => ..., Throws.TypeOf())` | `await Assert.That(() => ...).Throws()` / `.ThrowsExactly()` |
| Collection | `CollectionAssert.Contains` | `Assert.Contains` | `Assert.That(col, Has.Member(x))` | `await Assert.That(col).Contains(x)` |
| String | `StringAssert.Contains` | `Assert.Contains(str, sub)` | `Assert.That(str, Does.Contain(sub))` | `await Assert.That(str).Contains(sub)` |
-| Type | `Assert.IsInstanceOfType` | `Assert.IsAssignableFrom` | `Assert.That(x, Is.InstanceOf())` | `await Assert.That(x).IsTypeOf()` |
+| Type | `Assert.IsInstanceOfType` | `Assert.IsAssignableFrom` | `Assert.That(x, Is.InstanceOf())` | `await Assert.That(x).IsAssignableTo()` (use `.IsTypeOf()` for exact-type check) |
| Inconclusive | `Assert.Inconclusive()` | *skip via `[Fact(Skip)]`* | `Assert.Inconclusive()` | `Skip.Test("reason")` (no true inconclusive state) |
| Fail | `Assert.Fail()` | `Assert.Fail()` (.NET 10+) | `Assert.Fail()` | `Assert.Fail()` |
@@ -51,7 +51,7 @@ Third-party assertion libraries: `Should*` (Shouldly), `.Should()` (FluentAssert
| MSTest | `[Ignore]` | `[Ignore("reason")]` |
| xUnit | `[Fact(Skip = "reason")]` | *(reason is required)* |
| NUnit | `[Ignore("reason")]` | *(reason is required)* |
-| TUnit | `[Skip("reason")]` | *(reason is required)* |
+| TUnit | `[Skip("reason")]` | *(reason is required; also valid at class and assembly scope, e.g. `[assembly: Skip("…")]`. Dynamic in-test skipping via `Skip.Test("reason")`.)* |
| Conditional | `#if false` / `#if NEVER` | *(no reason possible)* |
## Exception Handling — Idiomatic Alternatives
From 5cc11d16ad39dbf83ea6c28383511e47a872d3f8 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Tue, 19 May 2026 20:30:50 +0100
Subject: [PATCH 4/4] Spell out TUnit assertion alternatives as complete
awaited expressions
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The Boolean, Null, Exception, and Type rows in the assertions table
previously used a `await Assert.That(x).IsTrue()` / `.IsFalse()` shorthand
for the alternative form. A reader copy-pasting just the second fragment
would end up with an un-awaited assertion that silently passes — which
contradicts the note immediately below the table about always awaiting.
Each alternative is now a complete, independently copy-pasteable expression.
Addresses the inline review on dotnet/skills#677.
---
.../dotnet-test/skills/dotnet-test-frameworks/SKILL.md | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/plugins/dotnet-test/skills/dotnet-test-frameworks/SKILL.md b/plugins/dotnet-test/skills/dotnet-test-frameworks/SKILL.md
index 3a840502ab..18c42d84a6 100644
--- a/plugins/dotnet-test/skills/dotnet-test-frameworks/SKILL.md
+++ b/plugins/dotnet-test/skills/dotnet-test-frameworks/SKILL.md
@@ -23,12 +23,12 @@ Language-specific detection patterns for .NET test frameworks (MSTest, xUnit, NU
| Category | MSTest | xUnit | NUnit | TUnit |
| -------- | ------ | ----- | ----- | ----- |
| Equality | `Assert.AreEqual` | `Assert.Equal` | `Assert.That(x, Is.EqualTo(y))` | `await Assert.That(x).IsEqualTo(y)` |
-| Boolean | `Assert.IsTrue` / `Assert.IsFalse` | `Assert.True` / `Assert.False` | `Assert.That(x, Is.True)` | `await Assert.That(x).IsTrue()` / `.IsFalse()` |
-| Null | `Assert.IsNull` / `Assert.IsNotNull` | `Assert.Null` / `Assert.NotNull` | `Assert.That(x, Is.Null)` | `await Assert.That(x).IsNull()` / `.IsNotNull()` |
-| Exception | `Assert.Throws()` / `Assert.ThrowsExactly()` | `Assert.Throws()` | `Assert.That(() => ..., Throws.TypeOf())` | `await Assert.That(() => ...).Throws()` / `.ThrowsExactly()` |
+| Boolean | `Assert.IsTrue` / `Assert.IsFalse` | `Assert.True` / `Assert.False` | `Assert.That(x, Is.True)` | `await Assert.That(x).IsTrue()` / `await Assert.That(x).IsFalse()` |
+| Null | `Assert.IsNull` / `Assert.IsNotNull` | `Assert.Null` / `Assert.NotNull` | `Assert.That(x, Is.Null)` | `await Assert.That(x).IsNull()` / `await Assert.That(x).IsNotNull()` |
+| Exception | `Assert.Throws()` / `Assert.ThrowsExactly()` | `Assert.Throws()` | `Assert.That(() => ..., Throws.TypeOf())` | `await Assert.That(() => ...).Throws()` / `await Assert.That(() => ...).ThrowsExactly()` |
| Collection | `CollectionAssert.Contains` | `Assert.Contains` | `Assert.That(col, Has.Member(x))` | `await Assert.That(col).Contains(x)` |
| String | `StringAssert.Contains` | `Assert.Contains(str, sub)` | `Assert.That(str, Does.Contain(sub))` | `await Assert.That(str).Contains(sub)` |
-| Type | `Assert.IsInstanceOfType` | `Assert.IsAssignableFrom` | `Assert.That(x, Is.InstanceOf())` | `await Assert.That(x).IsAssignableTo()` (use `.IsTypeOf()` for exact-type check) |
+| Type | `Assert.IsInstanceOfType` | `Assert.IsAssignableFrom` | `Assert.That(x, Is.InstanceOf())` | `await Assert.That(x).IsAssignableTo()` (use `await Assert.That(x).IsTypeOf()` for exact-type check) |
| Inconclusive | `Assert.Inconclusive()` | *skip via `[Fact(Skip)]`* | `Assert.Inconclusive()` | `Skip.Test("reason")` (no true inconclusive state) |
| Fail | `Assert.Fail()` | `Assert.Fail()` (.NET 10+) | `Assert.Fail()` | `Assert.Fail()` |