원클릭으로
tunit
Best practices for writing clean, maintainable, and robust tests with TUnit.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Best practices for writing clean, maintainable, and robust tests with TUnit.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
C# 14 coding standards. Invoke this skill always when working in C# language.
Rust coding standards for production code. Invoke this skill always when working in Rust language.
Expert guide for writing git commit messages following the Conventional Commits 1.0.0 specification.
Use `chromium --dump-dom` to inspect the fully rendered DOM after JavaScript execution. For debugging layout/scrolling issues and scraping dynamic pages.
Multi-repository codebase exploration. Research library internals, find code patterns, understand architecture, compare implementations across GitHub/npm/PyPI/crates. Use when needing deep understanding of how libraries work, finding implementations across open source, or exploring remote repository structure.
| name | tunit |
| description | Best practices for writing clean, maintainable, and robust tests with TUnit. |
This guide covers best practices for writing clean, maintainable, and robust tests with TUnit. Following these patterns will help you create a test suite that's easy to understand and maintain over time.
Good test names clearly describe what's being tested and what's expected. A common pattern is Method_Scenario_ExpectedBehavior:
// ✅ Good: Clearly describes what's being tested
[Test]
public async Task CalculateTotal_WithDiscount_ReturnsReducedPrice()
{
var calculator = new PriceCalculator();
var result = calculator.CalculateTotal(100, discount: 0.2);
await Assert.That(result).IsEqualTo(80);
}
// ❌ Bad: Vague and unclear
[Test]
public async Task Test1()
{
var calculator = new PriceCalculator();
var result = calculator.CalculateTotal(100, 0.2);
await Assert.That(result).IsEqualTo(80);
}
You can also use sentence-like names that read naturally:
[Test]
public async Task When_discount_is_applied_total_is_reduced()
{
// Test implementation
}
[Test]
public async Task Should_return_reduced_price_when_discount_applied()
{
// Test implementation
}
Pick a naming convention and stick to it throughout your project for consistency.
Organize your tests to mirror your production code structure:
MyApp/
Services/
OrderService.cs
PaymentService.cs
MyApp.Tests/
Services/
OrderServiceTests.cs
PaymentServiceTests.cs
This makes it easy to find tests and keeps your test suite organized as your codebase grows.
Use nested classes or separate test classes to group related test scenarios:
public class OrderServiceTests
{
public class CreateOrder
{
[Test]
public async Task Creates_order_with_valid_data()
{
// Test implementation
}
[Test]
public async Task Throws_exception_when_user_not_found()
{
// Test implementation
}
}
public class CancelOrder
{
[Test]
public async Task Cancels_order_successfully()
{
// Test implementation
}
[Test]
public async Task Throws_when_order_already_shipped()
{
// Test implementation
}
}
}
Each test file should focus on testing a single class or component. If your test file is getting large (>500 lines), consider splitting it into multiple files or using nested classes.
Use the most specific assertion available for better failure messages:
// ✅ Good: Specific assertion with clear failure message
await Assert.That(result).IsEqualTo(5);
// Failure: Expected 5 but was 3
// ❌ Okay but less helpful: Generic boolean assertion
await Assert.That(result == 5).IsTrue();
// Failure: Expected true but was false
Each test should verify one specific behavior. Multiple assertions are fine if they're testing different aspects of the same behavior:
// ✅ Good: Multiple assertions testing one behavior (user creation)
[Test]
public async Task CreateUser_SetsAllProperties()
{
var user = await userService.CreateUser("john@example.com", "John Doe");
await Assert.That(user.Email).IsEqualTo("john@example.com");
await Assert.That(user.Name).IsEqualTo("John Doe");
await Assert.That(user.CreatedAt).IsNotEqualTo(default(DateTime));
}
// ❌ Bad: Testing multiple unrelated behaviors
[Test]
public async Task UserService_Works()
{
var user = await userService.CreateUser("john@example.com", "John");
await Assert.That(user.Email).IsEqualTo("john@example.com");
await userService.DeleteUser(user.Id);
var deleted = await userService.GetUser(user.Id);
await Assert.That(deleted).IsNull();
}
TUnit assertions are async and must be awaited. Forgetting await means the assertion never runs:
// ❌ Wrong: Assertion returns Task that's never awaited
[Test]
public async Task MyTest()
{
Assert.That(result).IsEqualTo(5); // Test passes without checking!
}
// ✅ Correct: Assertion is awaited and executed
[Test]
public async Task MyTest()
{
await Assert.That(result).IsEqualTo(5);
}
The compiler will warn you about unawaited tasks, but watch for this common mistake.
TUnit provides several hooks for test lifecycle management. Use them to keep your test logic clean:
public class DatabaseTests
{
private TestDatabase? _database;
[Before(Test)]
public async Task SetupDatabase()
{
_database = await TestDatabase.CreateAsync();
}
[After(Test)]
public async Task CleanupDatabase()
{
if (_database != null)
await _database.DisposeAsync();
}
[Test]
public async Task Can_insert_record()
{
// Database is ready to use
await _database!.InsertAsync(new Record { Id = 1 });
var result = await _database.GetAsync(1);
await Assert.That(result).IsNotNull();
}
}
[Before(Test)] / [After(Test)]: Runs before/after each test (most common)
[Before(Class)] / [After(Class)]: Runs once per test class
[Before(Assembly)] / [After(Assembly)]: Runs once per test assembly
For expensive setup that needs to be shared across tests (like web servers, databases, or containers), use [ClassDataSource<>] with shared types and IAsyncInitializer/IAsyncDisposable:
// ✅ Best: Shared resource with ClassDataSource
public class TestWebServer : IAsyncInitializer, IAsyncDisposable
{
public WebApplicationFactory<Program>? Factory { get; private set; }
public async Task InitializeAsync()
{
Factory = new WebApplicationFactory<Program>();
await Task.CompletedTask;
}
public async ValueTask DisposeAsync()
{
if (Factory != null)
await Factory.DisposeAsync();
}
}
[ClassDataSource<TestWebServer>(Shared = SharedType.PerTestSession)]
public class ApiTests(TestWebServer server)
{
[Test]
public async Task Can_call_endpoint()
{
var client = server.Factory!.CreateClient();
var response = await client.GetAsync("/api/health");
await Assert.That(response.IsSuccessStatusCode).IsTrue();
}
[Test]
public async Task Can_get_users()
{
var client = server.Factory!.CreateClient();
var response = await client.GetAsync("/api/users");
await Assert.That(response.IsSuccessStatusCode).IsTrue();
}
}
Why this is better:
SharedType.PerTestSessionIAsyncInitializer/IAsyncDisposableShared Type Options:
SharedType.PerTestSession: One instance for entire test run, shared across assemblies (best for expensive resources)SharedType.PerClass: One instance per test classSharedType.None: New instance per test (default)You can also use hooks, but they're less flexible:
// ❌ Less flexible: Using hooks for shared setup
public class ApiTests
{
private static WebApplicationFactory<Program>? _factory;
[Before(Class)]
public static async Task StartServer()
{
_factory = new WebApplicationFactory<Program>();
}
[After(Class)]
public static async Task StopServer()
{
_factory?.Dispose();
}
// Tests use static _factory field
}
Keep your setup code simple and focused. If setup is complex, extract it to helper methods:
// ✅ Good: Simple setup with extracted helpers
[Before(Test)]
public async Task Setup()
{
_database = await CreateTestDatabase();
_testUser = await CreateTestUser();
}
private async Task<TestDatabase> CreateTestDatabase()
{
var db = await TestDatabase.CreateAsync();
await db.SeedDefaultData();
return db;
}
// ❌ Bad: Complex setup logic in hook
[Before(Test)]
public async Task Setup()
{
_database = await TestDatabase.CreateAsync();
await _database.ExecuteAsync("CREATE TABLE Users (...)");
await _database.ExecuteAsync("INSERT INTO Users VALUES (...)");
await _database.ExecuteAsync("CREATE TABLE Orders (...)");
// ... lots more setup code
}
TUnit runs tests in parallel for better performance. Write your tests to be independent:
// ✅ Good: Test is self-contained and independent
[Test]
public async Task Can_create_order()
{
var orderId = Guid.NewGuid(); // Unique ID
var order = new Order { Id = orderId, Total = 100 };
await orderService.CreateAsync(order);
var result = await orderService.GetAsync(orderId);
await Assert.That(result).IsNotNull();
}
Some tests can't run in parallel (database tests, file system tests). Use [NotInParallel]:
// Tests that modify shared state
[Test, NotInParallel]
public async Task Updates_configuration_file()
{
await ConfigurationManager.SetAsync("key", "value");
var result = await ConfigurationManager.GetAsync("key");
await Assert.That(result).IsEqualTo("value");
}
When tests need to run in a specific order, use [DependsOn] instead of NotInParallel with Order:
// ✅ Good: Use DependsOn for ordering while maintaining parallelism
[Test]
public async Task Step1_CreateUser()
{
// Runs first
}
[Test]
[DependsOn(nameof(Step1_CreateUser))]
public async Task Step2_UpdateUser()
{
// Runs after Step1_CreateUser completes
// Other unrelated tests can still run in parallel
}
[Test]
[DependsOn(nameof(Step2_UpdateUser))]
public async Task Step3_DeleteUser()
{
// Runs after Step2_UpdateUser completes
}
Why [DependsOn] is better:
You can also use NotInParallel with Order, but this forces sequential execution:
// ❌ Less flexible: Forces all tests to run sequentially
[Test, NotInParallel(Order = 1)]
public async Task Step1_CreateUser()
{
// Runs first
}
[Test, NotInParallel(Order = 2)]
public async Task Step2_UpdateUser()
{
// Runs second, but blocks all other tests
}
Important: If tests need ordering, they might be too tightly coupled. Consider:
Refactoring into a single test
Using proper setup/teardown
Making tests truly independent
Group related tests that can't run in parallel with each other but can run in parallel with other groups:
public class FileSystemTests
{
// These tests can't run in parallel with each other
// but can run in parallel with DatabaseTests
[Test, NotInParallel("FileGroup")]
public async Task Test1_WritesFile()
{
// Test implementation
}
[Test, NotInParallel("FileGroup")]
public async Task Test2_ReadsFile()
{
// Test implementation
}
}
public class DatabaseTests
{
[Test, NotInParallel("DbGroup")]
public async Task Test1_InsertsRecord()
{
// Runs in parallel with FileSystemTests
}
}
Each test should be completely independent and not rely on other tests:
// ❌ Bad: Tests depend on execution order
private static User? _user;
[Test]
public async Task Test1_CreateUser()
{
_user = await userService.CreateAsync("john@example.com");
}
[Test]
public async Task Test2_UpdateUser()
{
// Assumes Test1 ran first!
_user!.Name = "Jane Doe";
await userService.UpdateAsync(_user);
}
// ✅ Good: Each test is independent
[Test]
public async Task Can_create_user()
{
var user = await userService.CreateAsync("john@example.com");
await Assert.That(user.Email).IsEqualTo("john@example.com");
}
[Test]
public async Task Can_update_user()
{
var user = await userService.CreateAsync("jane@example.com");
user.Name = "Jane Doe";
await userService.UpdateAsync(user);
var updated = await userService.GetAsync(user.Id);
await Assert.That(updated.Name).IsEqualTo("Jane Doe");
}
Important: TUnit creates a new instance of your test class for each test method. Don't rely on instance fields to share state:
// ❌ Bad: Trying to share instance state between tests
public class MyTests
{
private int _value; // Different instance per test!
[Test, NotInParallel]
public void Test1()
{
_value = 99;
}
[Test, NotInParallel]
public async Task Test2()
{
await Assert.That(_value).IsEqualTo(99); // Fails! _value is 0
}
}
// ✅ Good: Use static fields if you really need shared state
public class MyTests
{
private static int _value; // Shared across all tests
[Test, NotInParallel]
public void Test1()
{
_value = 99;
}
[Test, NotInParallel]
public async Task Test2()
{
await Assert.That(_value).IsEqualTo(99); // Works!
}
}
But seriously: if tests need to share state, reconsider your design. It's usually better to make tests independent.
Tests should be simple and easy to understand. Avoid complex conditionals, loops, or calculations:
// ❌ Bad: Complex logic in test
[Test]
public async Task CalculatesTotals()
{
var items = await GetItems();
decimal expected = 0;
foreach (var item in items)
{
if (item.IsDiscounted)
expected += item.Price * 0.8m;
else
expected += item.Price;
}
var result = calculator.CalculateTotal(items);
await Assert.That(result).IsEqualTo(expected);
}
// ✅ Good: Simple, explicit test
[Test]
public async Task CalculateTotal_WithMixedItems()
{
var items = new[]
{
new Item { Price = 100, IsDiscounted = false }, // 100
new Item { Price = 50, IsDiscounted = true } // 40
};
var result = calculator.CalculateTotal(items);
await Assert.That(result).IsEqualTo(140);
}
If your test has complex logic, you're essentially writing code to test code. Keep it simple!
Don't mock everything. Use real implementations when they're fast and reliable:
// ❌ Bad: Mocking things that don't need mocking
[Test]
public async Task ProcessOrder()
{
var mockLogger = new Mock<ILogger>();
var mockValidator = new Mock<IValidator>();
var mockCalculator = new Mock<IPriceCalculator>();
var mockRepository = new Mock<IOrderRepository>();
// So much setup...
}
// ✅ Good: Only mock expensive or external dependencies
[Test]
public async Task ProcessOrder()
{
var logger = new NullLogger(); // Real lightweight implementation
var validator = new OrderValidator(); // Real validator is fast
var calculator = new PriceCalculator(); // Simple calculations
var mockRepository = new Mock<IOrderRepository>(); // Mock database
// Much simpler!
}
Mock external dependencies (databases, APIs, file systems) but use real implementations for simple logic.
Test behavior, not implementation. Your tests should verify what the code does, not how it does it:
// ❌ Bad: Testing internal implementation
[Test]
public async Task ProcessOrder_CallsRepositorySaveMethod()
{
var mockRepository = new Mock<IOrderRepository>();
var service = new OrderService(mockRepository.Object);
await service.ProcessOrder(order);
// Verifying method calls instead of behavior
mockRepository.Verify(r => r.Save(It.IsAny<Order>()), Times.Once);
}
// ✅ Good: Testing actual behavior
[Test]
public async Task ProcessOrder_SavesOrderToDatabase()
{
var repository = new InMemoryOrderRepository();
var service = new OrderService(repository);
await service.ProcessOrder(order);
// Verifying the result
var saved = await repository.GetAsync(order.Id);
await Assert.That(saved).IsNotNull();
await Assert.That(saved.Status).IsEqualTo(OrderStatus.Processed);
}
Tests that verify implementation details are brittle and break when you refactor.
TUnit is designed for performance at scale. Follow these guidelines to keep your test suite fast:
Use AOT mode for faster test discovery and lower memory usage
Keep data sources lightweight (see Performance Best Practices)
Limit matrix test combinations to avoid test explosion
[NotInParallel] when absolutely necessary--maximum-parallel-tests CLI flag or [assembly: ParallelLimiter<T>] attribute[Before(Test)] hooks - use class or assembly-level hooks for shared resourcesTests should be fast. If a test takes more than a few seconds, look for optimization opportunities:
// ❌ Slow: Real HTTP calls
[Test]
public async Task GetUserData()
{
var client = new HttpClient();
var response = await client.GetAsync("https://api.example.com/users");
// Slow and unreliable
}
// ✅ Fast: Use in-memory test doubles
[Test]
public async Task GetUserData()
{
var client = new TestHttpClient(); // In-memory fake
var response = await client.GetAsync("/users");
// Fast and reliable
}
For detailed performance guidance, see Performance Best Practices.
Following these best practices will help you:
Write tests that are easy to understand and maintain
Create a fast, reliable test suite that scales
Catch bugs without introducing brittle tests
Make your codebase more maintainable over time
Remember: good tests are simple, focused, independent, and fast. When in doubt, ask yourself: "Will someone else understand what this test is doing and why it might fail?"