Claude
Skills
Sign in
Back

dotnet-tunit-test

Included with Lifetime
$97 forever

Guidelines for writing TUnit tests in .NET, including setup, assertions, async testing, and best practices. Use when writing unit tests with TUnit framework, setting up TUnit in a .NET project, or migrating from other test frameworks to TUnit.

Writing & Docs

What this skill does


# Testing with TUnit

## When to Use This Skill

Use this skill when:
- Creating a new TUnit test project or adding TUnit to an existing solution
- Writing unit, integration, or acceptance tests using TUnit
- Migrating tests from xUnit, NUnit, or MSTest to TUnit
- Configuring data-driven tests with `[Arguments]`, `[MethodDataSource]`, or `[ClassDataSource]`
- Setting up test lifecycle hooks (`[Before]`/`[After]`)
- Controlling parallelism with `[NotInParallel]`, `[DependsOn]`, or parallel groups
- Writing ASP.NET Core integration tests with `TUnit.AspNetCore`
- Configuring TUnit for CI/CD pipelines with coverage and TRX reports

---

## What is TUnit?

TUnit is a modern, source-generated testing framework for .NET built on the Microsoft Testing Platform. Key characteristics:

- **Source generated** - Tests are discovered at compile time, not via reflection
- **Parallel by default** - Tests run concurrently for speed
- **Async-first assertions** - All assertions must be awaited
- **New class instance per test** - Test classes are instantiated fresh for each test method
- **No `[TestClass]` attribute needed** - Only `[Test]` on methods
- **Native AOT and single-file support** - Works where reflection-based frameworks cannot
- **Built-in code coverage and TRX reports** - No need for Coverlet

---

## Installation

### From Template (Recommended)

```bash
dotnet new install TUnit.Templates
dotnet new TUnit -n "MyApp.Tests"
```

### Manual Setup

```bash
dotnet new console --name MyApp.Tests
cd MyApp.Tests
dotnet add package TUnit --prerelease
```

Remove any auto-generated `Program.cs` -- TUnit handles the entry point.

### Project File

```xml
<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net9.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
    </PropertyGroup>
    <ItemGroup>
        <PackageReference Include="TUnit" Version="*" />
    </ItemGroup>
</Project>
```

### CRITICAL: Do NOT Use These Packages

| Package | Why |
|---------|-----|
| `Microsoft.NET.Test.Sdk` | Breaks TUnit test discovery -- TUnit uses Microsoft.Testing.Platform, not VSTest |
| `coverlet.collector` / `coverlet.msbuild` | Incompatible with TUnit -- use the built-in `--coverage` flag instead |

### Global Usings

TUnit automatically provides global usings for `TUnit.Core`, `TUnit.Assertions`, and `TUnit.Assertions.Extensions`. You do not need explicit `using` statements in test files.

---

## Writing Tests

### Basic Test

```csharp
namespace MyApp.Tests;

public class CalculatorTests
{
    [Test]
    public async Task Add_TwoNumbers_ReturnsSum()
    {
        var result = 2 + 3;

        await Assert.That(result).IsEqualTo(5);
    }
}
```

### Test Method Signatures

```csharp
[Test]
public void SyncTest()           // Valid -- synchronous, no assertions
{
    var result = Calculate(2, 3);
}

[Test]
public async Task AsyncTest()    // Recommended -- required if using assertions
{
    await Assert.That(42).IsEqualTo(42);
}

// async void is NOT allowed -- compiler error
```

**Rule**: If you use `Assert.That(...)`, the test method **must** be `async Task` because assertions are awaitable.

---

## Assertions

All TUnit assertions follow the pattern `await Assert.That(actual).SomeCondition()`.

### Core Assertions

```csharp
// Equality
await Assert.That(result).IsEqualTo(5);
await Assert.That(result).IsNotEqualTo(0);

// Comparison
await Assert.That(score).IsGreaterThan(70);
await Assert.That(age).IsLessThanOrEqualTo(100);
await Assert.That(temp).IsBetween(20, 30);

// Boolean
await Assert.That(isValid).IsTrue();
await Assert.That(isDeleted).IsFalse();

// Null
await Assert.That(result).IsNotNull();
await Assert.That(optional).IsNull();

// Type
await Assert.That(obj).IsTypeOf<MyClass>();
```

### String Assertions

```csharp
await Assert.That(message).Contains("Hello");
await Assert.That(filename).StartsWith("test_");
await Assert.That(email).Matches(@"^[\w\.-]+@[\w\.-]+\.\w+$");
await Assert.That(input).IsNotEmpty();
```

### Collection Assertions

```csharp
await Assert.That(numbers).Contains(42);
await Assert.That(items).Count().IsEqualTo(5);
await Assert.That(list).IsNotEmpty();
await Assert.That(values).All(x => x > 0);
await Assert.That(numbers).IsEquivalentTo(new[] { 5, 4, 3, 2, 1 }); // order-independent
await Assert.That(numbers).IsInOrder();
```

### Exception Assertions

```csharp
// Basic exception testing
await Assert.That(() => int.Parse("not a number"))
    .Throws<FormatException>();

// Async exception testing
await Assert.That(async () => await FailingOperationAsync())
    .Throws<HttpRequestException>();

// Exact type (no subclasses)
await Assert.That(() => throw new ArgumentNullException())
    .ThrowsExactly<ArgumentNullException>();

// Exception message
await Assert.That(() => throw new InvalidOperationException("Operation failed"))
    .Throws<InvalidOperationException>()
    .WithMessage("Operation failed");

await Assert.That(() => throw new ArgumentException("The parameter 'userId' is invalid"))
    .Throws<ArgumentException>()
    .WithMessageContaining("userId");

// ArgumentException parameter name
await Assert.That(() => ValidateUser(null!))
    .Throws<ArgumentNullException>()
    .WithParameterName("user");

// Inner exceptions
await Assert.That(() => ThrowWithInner())
    .Throws<InvalidOperationException>()
    .WithInnerException()
    .Throws<FormatException>();

// No exception thrown
await Assert.That(() => int.Parse("42"))
    .ThrowsNothing();
```

### Chaining with And / Or

```csharp
await Assert.That(username)
    .IsNotNull()
    .And.IsNotEmpty()
    .And.Length().IsGreaterThan(3)
    .And.Length().IsLessThan(20);

await Assert.That(statusCode)
    .IsEqualTo(200)
    .Or.IsEqualTo(201)
    .Or.IsEqualTo(204);
```

### Assert.Multiple (Report All Failures)

```csharp
using (Assert.Multiple())
{
    await Assert.That(user.FirstName).IsEqualTo("John");
    await Assert.That(user.LastName).IsEqualTo("Doe");
    await Assert.That(user.Age).IsGreaterThan(18);
}
// All failures reported together, not just the first one
```

### Floating-Point Tolerance

```csharp
await Assert.That(3.14159).IsEqualTo(Math.PI).Within(0.001);
```

### CRITICAL: Always Await Assertions

```csharp
// WRONG -- assertion never executes, test always passes
Assert.That(result).IsEqualTo(5);

// CORRECT
await Assert.That(result).IsEqualTo(5);
```

TUnit includes a built-in analyzer that warns about unawaited assertions.

---

## Data-Driven Tests

### [Arguments] -- Compile-Time Constants

```csharp
[Test]
[Arguments(1, 1, 2)]
[Arguments(1, 2, 3)]
[Arguments(2, 2, 4)]
public async Task Add_ReturnsExpectedResult(int a, int b, int expected)
{
    await Assert.That(a + b).IsEqualTo(expected);
}
```

Supports metadata: `DisplayName`, `Categories`, `Skip`:

```csharp
[Test]
[Arguments("Chrome", "120")]
[Arguments("Safari", "17", Skip = "Safari not available in CI")]
public async Task BrowserTest(string browser, string version) { }
```

### [MethodDataSource] -- Dynamic/Complex Data

```csharp
public static class TestData
{
    public static IEnumerable<Func<(int A, int B, int Expected)>> AdditionCases()
    {
        yield return () => (1, 2, 3);
        yield return () => (2, 2, 4);
        yield return () => (5, 5, 10);
    }
}

public class MathTests
{
    [Test]
    [MethodDataSource(typeof(TestData), nameof(TestData.AdditionCases))]
    public async Task Add_WithData(int a, int b, int expected)
    {
        await Assert.That(a + b).IsEqualTo(expected);
    }
}
```

For reference types, return `Func<T>` (not `T`) to ensure each test gets a fresh instance.

### [ClassDataSource] -- Injectable Shared Resources

```csharp
public class TestWebServer : IAsyncInitializer, IAsyncDisposable
{
    public WebApplicationFactory<Program>? Factory { get; private set; }

    public async Task InitializeAsync()
    {
        Factory = new WebApplication

Related in Writing & Docs