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()` |