Categories
Agent Tooling (71)
- .agents/skills/agent-browser/references/authentication.md
- .agents/skills/agent-browser/references/commands.md
- .agents/skills/agent-browser/references/profiling.md
- .agents/skills/agent-browser/references/proxy-support.md
- .agents/skills/agent-browser/references/session-management.md
- .agents/skills/agent-browser/references/snapshot-refs.md
- .agents/skills/agent-browser/references/video-recording.md
- .agents/skills/agent-browser/SKILL.md
- .agents/skills/aspire-deployment/references/aws.md
- .agents/skills/aspire-deployment/references/azure.md
- .agents/skills/aspire-deployment/references/cicd.md
- .agents/skills/aspire-deployment/references/docker-compose.md
- .agents/skills/aspire-deployment/references/javascript.md
- .agents/skills/aspire-deployment/references/kubernetes.md
- .agents/skills/aspire-deployment/references/preflight.md
- .agents/skills/aspire-deployment/SKILL.md
- .agents/skills/aspire-monitoring/references/diagnostics-bridge.md
- .agents/skills/aspire-monitoring/references/monitoring.md
- .agents/skills/aspire-monitoring/references/playwright-handoff.md
- .agents/skills/aspire-monitoring/SKILL.md
- .agents/skills/aspire-orchestration/references/agent-workflows.md
- .agents/skills/aspire-orchestration/references/app-commands.md
- .agents/skills/aspire-orchestration/references/detection.md
- .agents/skills/aspire-orchestration/references/resource-management.md
- .agents/skills/aspire-orchestration/references/safety-guardrails.md
- .agents/skills/aspire-orchestration/SKILL.md
- .agents/skills/aspire/references/aspire-13-3-breaking-changes.md
- .agents/skills/aspire/SKILL.md
- .agents/skills/aspireify/references/apphost-wiring.md
- .agents/skills/aspireify/references/csharp-authoring.md
- .agents/skills/aspireify/references/docker-compose.md
- .agents/skills/aspireify/references/full-solution-apphosts.md
- .agents/skills/aspireify/references/javascript-apps.md
- .agents/skills/aspireify/references/opentelemetry.md
- .agents/skills/aspireify/references/scan-and-propose.md
- .agents/skills/aspireify/references/service-defaults.md
- .agents/skills/aspireify/references/typescript-authoring.md
- .agents/skills/aspireify/references/validation.md
- .agents/skills/aspireify/SKILL.md
- .agents/skills/caveman-compress/README.md
- .agents/skills/caveman-compress/SECURITY.md
- .agents/skills/caveman-compress/SKILL.md
- .agents/skills/caveman-help/README.md
- .agents/skills/caveman-help/SKILL.md
- .agents/skills/caveman-stats/README.md
- .agents/skills/caveman-stats/SKILL.md
- .agents/skills/caveman/README.md
- .agents/skills/caveman/SKILL.md
- .agents/skills/csharp-developer/SKILL.md
- .agents/skills/dotnet-inspect/SKILL.md
- .agents/skills/efcore-data-layer/SKILL.md
- .agents/skills/git/SKILL.md
- .agents/skills/microsoft-code-reference/SKILL.md
- .agents/skills/microsoft-docs/SKILL.md
- .agents/skills/microsoft-skill-creator/references/skill-templates.md
- .agents/skills/microsoft-skill-creator/SKILL.md
- .agents/skills/skill-creator/references/output-patterns.md
- .agents/skills/skill-creator/references/workflows.md
- .agents/skills/skill-creator/SKILL.md
- .agents/skills/tunit-testing/references/aspire-integration.md
- .agents/skills/tunit-testing/references/aspnetcore-integration.md
- .agents/skills/tunit-testing/references/assertions.md
- .agents/skills/tunit-testing/references/data-driven.md
- .agents/skills/tunit-testing/references/infrastructure-and-troubleshooting.md
- .agents/skills/tunit-testing/references/lifecycle-hooks.md
- .agents/skills/tunit-testing/SKILL.md
- AGENTS.md
- backend/AGENTS.md
- backend/tests/AGENTS.md
- Front/AGENT.md
- opencode.jsonc
name: tunit-testing description: Guide for creating tests using TUnit framework in backend/tests/. Use when the user asks to create tests, add test coverage, write unit tests, integration tests, or when working with test files in the backend test projects. Covers TUnit syntax, assertions, setup/cleanup hooks, data-driven tests, and project-specific patterns.
TUnit Testing
Overview
Write tests using the TUnit testing framework for the backend .NET projects. TUnit is a modern, high-performance testing framework built on the Microsoft Testing Platform with source generation for speed.
Project Structure
Tests are located in backend/tests/:
Kds.Ai.ApiService.Tests/- Integration tests for the API serviceEndpoints/- Endpoint-specific testsInfrastructure/- Test infrastructure (base classes, factories, test containers)
Quick Start
namespace Kds.Ai.ApiService.Tests.Endpoints;
public class MyFeatureTests : TestsBase
{
[Test]
public async Task GetEndpoint_WithNoData_ReturnsEmptyList()
{
// Arrange
var client = Factory.CreateClient();
// Act
var response = await client.GetAsync("/my-endpoint");
// Assert
await Assert.That(response).IsOk();
await Assert.That(response).HasJsonContent();
}
}Key points:
- Inherit from
TestsBasefor API integration tests - Use
[Test]attribute (no[TestClass]needed) - Methods must be
async Task - Always
awaitassertions - Naming:
Method_Scenario_ExpectedOutcome
Test Infrastructure
The project provides infrastructure in Infrastructure/:
TestsBase - Base class inheriting from WebApplicationTest<AppFactory, Program>
- Provides
Factoryproperty for accessing the test app
AppFactory - Configures test application:
- Sets up PostgreSQL testcontainer
- Runs EF migrations on initialization
- Overrides connection strings for test environment
PostgresDatabase - Shared PostgreSQL container (SharedType.PerTestSession)
Using Factory
The Factory property provides access to the test application:
// Create HTTP client
var client = Factory.CreateClient();
// Access DI container (Factory.Services is per-test scoped)
await using var scope = Factory.Services.CreateAsyncScope();
var service = scope.ServiceProvider.GetRequiredService<IMyService>();
// Access WebApplicationFactory directly
var webApp = Factory; // WebApplicationFactory<Program>Core Assertions
All assertions use Assert.That() and must be awaited:
// Equality & comparisons
await Assert.That(actual).IsEqualTo(expected);
await Assert.That(score).IsGreaterThan(70);
await Assert.That(temp).IsBetween(20, 30);
// Null & booleans
await Assert.That(result).IsNotNull();
await Assert.That(flag).IsTrue();
// Strings
await Assert.That(message).Contains("error");
await Assert.That(email).Matches(@"^[\w\.-]+@[\w\.-]+\.\w+$");
// Collections
await Assert.That(items).IsEmpty();
await Assert.That(numbers).Contains(42);
await Assert.That(list).Count().IsEqualTo(5);
// Chaining
await Assert.That(username)
.IsNotNull()
.And.IsNotEmpty()
.And.Length().IsGreaterThan(3);HTTP Response Assertions
TUnit provides built-in HTTP assertions:
var response = await client.GetAsync("/endpoint");
// Status codes
await Assert.That(response).IsOk(); // 200
await Assert.That(response).IsCreated(); // 201
await Assert.That(response).IsNoContent(); // 204
await Assert.That(response).IsBadRequest(); // 400
await Assert.That(response).IsUnauthorized(); // 401
await Assert.That(response).IsForbidden(); // 403
await Assert.That(response).IsNotFound(); // 404
await Assert.That(response).IsConflict(); // 409
await Assert.That(response).HasStatusCode(HttpStatusCode.Created);
// Range checks
await Assert.That(response).IsSuccessStatusCode();
await Assert.That(response).IsRedirectStatusCode();
await Assert.That(response).IsClientErrorStatusCode();
await Assert.That(response).IsServerErrorStatusCode();
// Content & headers
await Assert.That(response).HasJsonContent();
await Assert.That(response).HasContentType("application/json");
await Assert.That(response).HasHeader("X-Request-Id");
// Read and assert on content
var data = await response.Content.ReadFromJsonAsync<MyResponse>();
await Assert.That(data).IsNotNull();Test Attributes
[Test] // Marks method as test
public async Task MyTest() { }
[NotInParallel] // Prevents parallel execution
[Test]
public async Task SequentialTest() { }
[NotInParallel(Order = 1)] // Controls execution order
[Test]
public async Task FirstTest() { }Lifecycle Hooks
For detailed hook patterns, see references/lifecycle-hooks.md.
For ASP.NET Core integration testing patterns with TUnit.AspNetCore, see references/aspnetcore-integration.md.
For Aspire distributed application testing patterns with TUnit.Aspire, see references/aspire-integration.md.
For shared infrastructure orchestration, live-test opt-in patterns, and common troubleshooting notes, see references/infrastructure-and-troubleshooting.md.
Basic example:
public class MyTests : TestsBase
{
private MyService _service = null!;
[Before(Test)]
public async Task Setup()
{
_service = new MyService();
await _service.InitializeAsync();
}
[After(Test)]
public async Task Cleanup()
{
await _service.DisposeAsync();
}
}Data-Driven Tests
For comprehensive patterns, see references/data-driven.md.
Basic example:
[Test]
[Arguments(2, 3, 5)]
[Arguments(10, 20, 30)]
public async Task Add_ReturnsSum(int a, int b, int expected)
{
var result = Calculator.Add(a, b);
await Assert.That(result).IsEqualTo(expected);
}Common Patterns
API Endpoint Test
public class AlertEndpointsTests : TestsBase
{
[Test]
public async Task GetAlerts_WithNoData_ReturnsEmptyPage()
{
// Arrange
var client = Factory.CreateClient();
// Act
var response = await client.GetAsync("/alerts");
// Assert
await Assert.That(response).IsOk();
await Assert.That(response).HasJsonContent();
var alerts = await response.Content.ReadFromJsonAsync<AlertPageResponse>();
await Assert.That(alerts).IsNotNull();
await Assert.That(alerts.Items).IsEmpty();
}
}Database Test
[Test]
public async Task DatabaseTest()
{
await using var scope = Factory.Services.CreateAsyncScope();
var dbContext = scope.ServiceProvider.GetRequiredService<KdsAiDbContext>();
var entity = new MyEntity { Name = "Test" };
dbContext.MyEntities.Add(entity);
await dbContext.SaveChangesAsync();
var saved = await dbContext.MyEntities.FirstOrDefaultAsync(e => e.Name == "Test");
await Assert.That(saved).IsNotNull();
}Naming Conventions
- Test class:
{Feature}Tests(e.g.,AlertEndpointsTests) - Test method:
{Method}_{Scenario}_{ExpectedOutcome} GetAlerts_WithNoData_ReturnsEmptyPageCreateAlert_WithValidData_ReturnsCreatedAlertDeleteAlert_WhenNotFound_Returns404
Test Organization
backend/tests/Kds.Ai.ApiService.Tests/
├── Endpoints/
│ ├── AlertEndpointsTests.cs
│ └── ...
└── Infrastructure/
├── TestsBase.cs
├── AppFactory.cs
└── PostgresDatabase.csImportant Concepts
Test Instance Isolation
Each test gets a new instance of the test class. Instance fields are NOT shared:
public class MyTests
{
private int _value; // Different for each test
[Test]
public void Test1() { _value = 99; }
[Test]
public async Task Test2()
{
await Assert.That(_value).IsEqualTo(0); // Still 0, not 99
}
}Use static to share state (but be cautious about isolation):
private static int _sharedValue; // Shared across testsIsolating Shared Resources
When tests share resources like database tables, message queues, or cache keys, use the built-in isolation helpers:
protected override async Task SetupAsync()
{
// GetIsolatedName(baseName) - returns "Test_{UniqueId}_{baseName}"
var tableName = GetIsolatedName("todos"); // "Test_42_todos"
var queueName = GetIsolatedName("events"); // "Test_42_events"
// GetIsolatedPrefix(separator) - returns "test{separator}{UniqueId}{separator}"
var cachePrefix = GetIsolatedPrefix(); // "test_42_"
var dotPrefix = GetIsolatedPrefix("."); // "test.42."
await CreateTableAsync(tableName);
await CreateQueueAsync(queueName);
}Why this matters: TUnit runs tests in parallel by default. Without proper isolation, tests will interfere with each other and produce flaky failures.
Common resources that need isolation:
- Database tables: Use
GetIsolatedName("tablename") - Message queues/topics: Use
GetIsolatedName("queue") - Cache keys: Use
GetIsolatedPrefix()as a key prefix - Blob storage paths: Use
GetIsolatedPrefix()as a path prefix - Redis keys: Use
GetIsolatedPrefix()as a key prefix
See the TUnit ASP.NET Core docs for more details.
Parallelization
Tests run in parallel by default. Use [NotInParallel] to prevent race conditions.
Running Tests
# Run all tests
dotnet test
# Run specific project
dotnet test --project backend/tests/Kds.Ai.ApiService.Tests/Kds.Ai.ApiService.Tests/Kds.Ai.ApiService.Tests.csproj
# Filter by test class
dotnet test --project backend/tests/Kds.Ai.ApiService.Tests/Kds.Ai.ApiService.Tests/Kds.Ai.ApiService.Tests.csproj -- --treenode-filter "/*/*/TenantMonitorEndpointsTests/*"
# Filter by test name
dotnet test --project backend/tests/Kds.Ai.ApiService.Tests/Kds.Ai.ApiService.Tests/Kds.Ai.ApiService.Tests.csproj -- --treenode-filter "/*/*/*/GetEndpoint_WithNoData_ReturnsEmptyList"
# Verbose output
dotnet test --verbosity detailedTest Filters
TUnit runs on Microsoft.Testing.Platform, not VSTest.
- Do not use
dotnet test --filter ...for TUnit tests. - Use
--treenode-filterinstead. - In this repo, prefer passing the TUnit filter after
--when usingdotnet test: dotnet test --project <csproj> -- --treenode-filter "..."
Filter path syntax:
/<Assembly>/<Namespace>/<Class name>/<Test name>Useful examples:
- Class filter:
///TenantMonitorEndpointsTests/* - Test filter:
///*/SetAsync_WhenEnableRequestedAndAssignmentMissing_CreatesEnabledAssignment - Namespace filter:
//Kds.Ai.ApiService.Tests.Endpoints//*
Rules:
- Use
*as wildcard. --filteris silently rejected by MTP and often ends withZero tests ran.- On SDKs that accept it,
dotnet test --treenode-filter "..."also works, but-- --treenode-filteris safer across environments.
Best Practices
- Always await assertions - Required for assertions to execute
- Use descriptive names - Follow
Method_Scenario_Outcomepattern - One logical assertion per test - Focus on single behavior
- Inherit from TestsBase - For API integration tests
- Use Factory.CreateClient() - For HTTP testing
- Clean up resources - Use
[After]hooks - Avoid test interdependence - Each test should be independent
- Use
[NotInParallel]sparingly - Only when absolutely necessary - Prefer per-test isolation helpers - Use
SetupAsync(),GetIsolatedName(...), andGetIsolatedPrefix(...)for schemas, tables, queues, and keys when tests run in parallel - Gate live tests with config, not manual-only attributes - Default runs should skip live coverage with
Skip.Test(...)unless an explicit opt-in such asKDSAI_RUN_LIVE_TESTS=1is enabled - Use startup-time configuration for startup-time settings - If the SUT reads configuration during app startup, prefer
ConfigureStartupConfiguration(...), AppHost args, or host settings instead of later configuration hooks - Prefer TUnit-managed fixture chains for shared infrastructure - Use
ClassDataSource<T>and property injection instead of manually creating and disposing dependent shared fixtures - Use collection-equivalence assertions for buffers - For
byte[]and similar payload buffers, preferIsEquivalentTo(...)overIsEqualTo(...)
Additional Resources
For advanced topics, see:
- references/lifecycle-hooks.md - Setup/cleanup patterns
- references/data-driven.md - Data-driven testing
- references/assertions.md - Complete assertion reference
- references/aspnetcore-integration.md -
WebApplicationTest,TestWebApplicationFactory, and per-test configuration - references/aspire-integration.md -
AspireFixture, shared AppHost lifecycle, resource waiting, and distributed app diagnostics - references/infrastructure-and-troubleshooting.md - Shared dependency orchestration, live-test gating, and package/version pitfalls
- TUnit Documentation - Official docs