Skip to content

Commit d8851ef

Browse files
committed
feat: Add TUnit skill and references for testing framework, assertions, attributes, data-driven tests, and integration patterns
1 parent b31342b commit d8851ef

5 files changed

Lines changed: 733 additions & 0 deletions

File tree

.github/skills/tunit/SKILL.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
---
2+
name: tunit
3+
description: >
4+
Write, review, and fix TUnit tests in .NET projects. Use this skill whenever
5+
you're writing a new test class, adding assertions, creating data-driven tests,
6+
setting up test lifecycle hooks, or debugging why a test compiles but behaves
7+
unexpectedly. Also triggers for: migrating from xUnit/NUnit to TUnit, choosing
8+
the right assertion, using Bogus for test data, wiring up NSubstitute mocks,
9+
writing integration tests, or any question about parallelism and test ordering.
10+
Prefer this skill over guessing — TUnit's async-first API has several
11+
non-obvious patterns that differ from xUnit/NUnit.
12+
---
13+
14+
# TUnit Testing Skill
15+
16+
TUnit is a modern .NET testing framework that is async-first, source-generated,
17+
and runs on Microsoft.Testing.Platform.
18+
19+
---
20+
21+
## Quick-start anatomy
22+
23+
```csharp
24+
public class OrderHandlerTests
25+
{
26+
[Test]
27+
[Category("Unit")] // categorise for filtering
28+
public async Task PlaceOrder_Valid_ReturnsOk()
29+
{
30+
// Arrange …
31+
// Act …
32+
// Assert — always await the assertion
33+
await Assert.That(result).IsNotNull();
34+
await Assert.That(result.Status).IsEqualTo("Confirmed");
35+
}
36+
}
37+
```
38+
39+
Key rule: **every assertion line must be `await`-ed**. Forgetting the `await`
40+
silently skips the assertion.
41+
42+
---
43+
44+
## Reference files — read before writing code
45+
46+
| Topic | File |
47+
|-------|------|
48+
| Assertion API (equality, nulls, booleans, collections, strings, exceptions, multiple) | [references/assertions.md](references/assertions.md) |
49+
| All attributes (Test, Category, Arguments, Before/After, Retry, Repeat, NotInParallel…) | [references/attributes.md](references/attributes.md) |
50+
| Data-driven tests (Arguments, MethodDataSource, ClassDataSource) | [references/data-driven.md](references/data-driven.md) |
51+
| Integration test patterns (Aspire, test infrastructure, async helpers) | [references/integration-patterns.md](references/integration-patterns.md) |
52+
53+
---
54+
55+
## Core rules
56+
57+
```
58+
✅ [Test] async Task ❌ [Fact] / [TestMethod] / [TestCase]
59+
✅ await Assert.That(...) ❌ Assert.Equal / FluentAssertions
60+
✅ Per-test data creation ❌ shared mutable state between tests
61+
```
62+
63+
---
64+
65+
## Running tests
66+
67+
```bash
68+
dotnet test # all projects (parallel)
69+
dotnet test -- --maximum-parallel-tests 4 # cap parallelism
70+
dotnet test -- --treenode-filter "/*/*/*/*[Category=Unit]" # filter by category
71+
dotnet test --project tests/MyProject.Tests/MyProject.Tests.csproj
72+
```
73+
74+
---
75+
76+
## Common mistakes & fixes
77+
78+
| Mistake | Fix |
79+
|---------|-----|
80+
| Assertion silently skipped | Add `await` to every `Assert.That(…)` call |
81+
| Tests interfere with each other | Create all test data inside each test; never share mutable fields |
82+
| Polling for eventual consistency | Use condition helpers or WaitForConditionAsync patterns; never `Task.Delay` |
83+
| `Assert.Fail` not reached after exception | Use `Assert.That(…).Throws<T>()` pattern instead of try/catch |
84+
| Missing `[Before(Class)]` / `[After(Class)]` | Hooks must be `public static async Task`; see attributes reference |
85+
| Data-driven test parameters don't compile | Types in `[Arguments]` must exactly match method parameter types |
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# TUnit Assertion Reference
2+
3+
All assertions are async and must be `await`-ed.
4+
5+
```csharp
6+
await Assert.That(actual).IsEqualTo(expected);
7+
```
8+
9+
---
10+
11+
## Equality & comparison
12+
13+
```csharp
14+
await Assert.That(actual).IsEqualTo(expected);
15+
await Assert.That(actual).IsNotEqualTo(unexpected);
16+
await Assert.That(number).IsGreaterThan(0);
17+
await Assert.That(number).IsGreaterThanOrEqualTo(1);
18+
await Assert.That(number).IsLessThan(100);
19+
await Assert.That(number).IsLessThanOrEqualTo(99);
20+
await Assert.That(number).IsBetween(1, 99); // inclusive
21+
```
22+
23+
---
24+
25+
## Null & existence
26+
27+
```csharp
28+
await Assert.That(value).IsNull();
29+
await Assert.That(value).IsNotNull();
30+
```
31+
32+
---
33+
34+
## Boolean
35+
36+
```csharp
37+
await Assert.That(flag).IsTrue();
38+
await Assert.That(flag).IsFalse();
39+
```
40+
41+
---
42+
43+
## Type checks
44+
45+
```csharp
46+
await Assert.That(obj).IsTypeOf<MyType>();
47+
await Assert.That(obj).IsNotTypeOf<WrongType>();
48+
await Assert.That(obj).IsAssignableTo<IMyInterface>();
49+
```
50+
51+
---
52+
53+
## Collections
54+
55+
```csharp
56+
await Assert.That(list).IsEmpty();
57+
await Assert.That(list).IsNotEmpty();
58+
await Assert.That(list).Contains(item);
59+
await Assert.That(list).DoesNotContain(item);
60+
await Assert.That(list).Count().IsEqualTo(3);
61+
await Assert.That(list).HasSingleItem();
62+
```
63+
64+
---
65+
66+
## Strings
67+
68+
```csharp
69+
await Assert.That(text).IsEqualTo("exact");
70+
await Assert.That(text).IsEmpty();
71+
await Assert.That(text).IsNotEmpty();
72+
await Assert.That(text).Contains("sub");
73+
await Assert.That(text).DoesNotContain("absent");
74+
await Assert.That(text).StartsWith("prefix");
75+
await Assert.That(text).EndsWith("suffix");
76+
await Assert.That(text).Matches(@"^\d{4}-\d{2}-\d{2}$"); // regex
77+
```
78+
79+
---
80+
81+
## Exceptions
82+
83+
```csharp
84+
// Synchronous throw wrapped in a lambda
85+
await Assert.ThrowsAsync<ArgumentException>(() =>
86+
{
87+
SomeMethod();
88+
return Task.CompletedTask;
89+
});
90+
91+
// Async throw
92+
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
93+
await SomeAsyncMethod());
94+
95+
// Capture and inspect the exception
96+
var ex = await Assert.ThrowsAsync<ApiException>(async () =>
97+
await client.GetBookAsync(id));
98+
await Assert.That(ex!.StatusCode).IsEqualTo(HttpStatusCode.NotFound);
99+
100+
// Verify NO exception is thrown (rarely needed — just call the method directly)
101+
await Assert.DoesNotThrowAsync(async () => await SafeMethod());
102+
```
103+
104+
---
105+
106+
## Multiple assertions grouped (Assert.Multiple)
107+
108+
Use `Assert.Multiple()` to run several assertions and collect all failures before
109+
reporting, rather than stopping at the first failure.
110+
111+
```csharp
112+
[Test]
113+
public async Task Constructor_SetsAllProperties()
114+
{
115+
var date = new PartialDate(2023, 5, 15);
116+
117+
using var scope = Assert.Multiple();
118+
await Assert.That(date.Year).IsEqualTo(2023);
119+
await Assert.That(date.Month).IsEqualTo(5);
120+
await Assert.That(date.Day).IsEqualTo(15);
121+
}
122+
```
123+
124+
> `Assert.Multiple()` returns an `IDisposable`; always wrap in `using var scope`.
125+
126+
---
127+
128+
## Chained member assertions (single await, multiple properties)
129+
130+
```csharp
131+
await Assert.That(user)
132+
.IsNotNull()
133+
.And.Member(u => u.Email, e => e.IsEqualTo("john@example.com"))
134+
.And.Member(u => u.Age, a => a.IsGreaterThan(18));
135+
```
136+
137+
---
138+
139+
## Assert.Fail
140+
141+
Use when a code path should never be reached:
142+
143+
```csharp
144+
Assert.Fail("Expected an exception, but none was thrown.");
145+
```
146+
147+
---
148+
149+
## Awaiting assertions — why it matters
150+
151+
TUnit returns a lazy assertion object from `Assert.That(…)`. The assertion is only
152+
evaluated when `await`-ed. Missing the `await` means the check is never run and
153+
the test passes silently even when the code is wrong. This is the most common
154+
mistake when migrating from xUnit/NUnit.

0 commit comments

Comments
 (0)