diff --git a/plugins/dotnet-test/skills/dotnet-test-frameworks/SKILL.md b/plugins/dotnet-test/skills/dotnet-test-frameworks/SKILL.md index cfde1b7dce..18c42d84a6 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()` / `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 `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()` | -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 @@ -49,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 @@ -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`. 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 - +