Reference data for .NET test framework detection patterns, assertion APIs, skip annotations, setup/teardown methods, and common test smell indicators across MSTest, xUnit, NUnit, and TUnit. Loaded by test analysis skills (test-anti-patterns) as framework-specific lookup tables.
npx skills add https://github.com/microsoft/testfx --skill dotnet-test-frameworks
Language-specific detection patterns for .NET test frameworks (MSTest, xUnit, NUnit, TUnit).
| Framework | Test class markers | Test method markers |
| --------- | ------------------ | ------------------- |
| MSTest | [TestClass] | [TestMethod], [DataTestMethod] |
| xUnit | *(none — convention-based)* | [Fact], [Theory] |
| NUnit | [TestFixture] | [Test], [TestCase], [TestCaseSource] |
| TUnit | *(none — convention-based)* | [Test] |
| 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<T>() / Assert.ThrowsExactly<T>() | Assert.Throws<T>() | Assert.That(() => ..., Throws.TypeOf<T>()) | await Assert.That(() => ...).Throws<T>() / await Assert.That(() => ...).ThrowsExactly<T>() |
| 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<T>()) | await Assert.That(x).IsAssignableTo<T>() (use await Assert.That(x).IsTypeOf<T>() 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() |
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.
| Pattern | Example |
| ------- | ------- |
| Thread sleep | Thread.Sleep(2000) |
| Task delay | await Task.Delay(1000) |
| SpinWait | SpinWait.SpinUntil(() => condition, timeout) |
| Framework | Annotation | With reason |
| --------- | ---------- | ----------- |
| MSTest | [Ignore] | [Ignore("reason")] |
| xUnit | [Fact(Skip = "reason")] | *(reason is required)* |
| NUnit | [Ignore("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)* |
When a test uses try/catch to verify exceptions, suggest the framework-native alternative:
MSTest:
// Instead of try/catch (matches exact type):
var ex = Assert.ThrowsExactly<InvalidOperationException>(
() => processor.ProcessOrder(emptyOrder));
Assert.AreEqual("Order must contain at least one item", ex.Message);
// Or (also matches derived types):
var ex = Assert.Throws<InvalidOperationException>(
() => processor.ProcessOrder(emptyOrder));
Assert.AreEqual("Order must contain at least one item", ex.Message);
xUnit:
var ex = Assert.Throws<InvalidOperationException>(
() => processor.ProcessOrder(emptyOrder));
Assert.Equal("Order must contain at least one item", ex.Message);
NUnit:
var ex = Assert.Throws<InvalidOperationException>(
() => processor.ProcessOrder(emptyOrder));
Assert.That(ex.Message, Is.EqualTo("Order must contain at least one item"));
TUnit:
await Assert.That(() => processor.ProcessOrder(emptyOrder))
.Throws<InvalidOperationException>()
.WithMessage("Order must contain at least one item");
// Or, for exact-type matching (no derived types):
await Assert.That(() => processor.ProcessOrder(emptyOrder))
.ThrowsExactly<InvalidOperationException>();
| Smell indicator | What to look for |
| --------------- | ---------------- |
| File system | File.ReadAllText, File.Exists, File.WriteAllBytes, Directory.GetFiles, Path.Combine with hard-coded paths |
| Database | SqlConnection, DbContext (without in-memory provider), SqlCommand |
| Network | HttpClient without HttpMessageHandler override, WebRequest, TcpClient |
| Environment | Environment.GetEnvironmentVariable, Environment.CurrentDirectory |
| Acceptable | MemoryStream, StringReader, InMemory database providers, custom DelegatingHandler |
Recognize these as integration tests (adjust smell severity accordingly):
Integration, E2E, EndToEnd, or Acceptance[TestCategory("Integration")] (MSTest)[Trait("Category", "Integration")] (xUnit)[Category("Integration")] (NUnit, TUnit).IntegrationTests or .E2ETests| Framework | Setup | Teardown |
| --------- | ----- | -------- |
| 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<T> | 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.
Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
Comprehensive GitHub release orchestration with AI swarm coordination for automated versioning, testing, deployment, and rollback management
Migrate test files from `as` type assertions to @total-typescript/shoehorn. Use when user mentions shoehorn, wants to replace `as` in tests, or needs partial test data.
Modern JavaScript/TypeScript development with Bun runtime. Covers package management, bundling, testing, and migration from Node.js. Use when working with Bun, optimizing JS/TS development speed, or migrating from Node.js to Bun.
You are a dependency management expert specializing in safe, incremental upgrades of project dependencies. Plan and execute dependency updates with minimal risk, proper testing, and clear migration pa
Master systematic debugging techniques, profiling tools, and root cause analysis to efficiently track down bugs across any codebase or technology stack. Use when investigating bugs, performance issues, or unexpected behavior.
Opinionated backend development standards for Node.js + Express + TypeScript microservices. Covers layered architecture, BaseController pattern, dependency injection, Prisma repositories, Zod validation, unifiedConfig, Sentry error tracking, async safety, and testing discipline.
Best practices for writing JavaScript/TypeScript tests using Jest, including mocking strategies, test structure, and common patterns.
Take microsoft/dotnet-test-frameworks from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.