Performance Best Practices
This guide provides recommendations for optimizing test performance and ensuring your TUnit test suite runs efficiently.
Test Discovery Performance
Use AOT Mode
TUnit's AOT (Ahead-of-Time) compilation mode provides the best performance for test discovery:
<PropertyGroup>
<IsAotCompatible>true</IsAotCompatible>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
</PropertyGroup>
Benefits:
- Faster test discovery
- Lower memory usage
- Better performance in CI/CD pipelines
Optimize Data Sources
Keep Data Generation Lightweight
// ❌ Bad: Heavy computation during discovery
public static IEnumerable<User> GetTestUsers()
{
// This runs during test discovery!
var users = DatabaseQuery.GetAllUsers();
return users.Where(u => u.IsActive);
}
// ✅ Good: Lightweight data generation
public static IEnumerable<User> GetTestUsers()
{
yield return new User { Id = 1, Name = "Test User 1" };
yield return new User { Id = 2, Name = "Test User 2" };
}
Use Lazy Data Loading
// ✅ Good: Defer expensive operations until test execution
[Test]
[MethodDataSource<LazyDataProvider>(nameof(LazyDataProvider.GetIds))]
public async Task TestWithLazyData(int id)
{
// Load full data only during test execution
var user = await LoadUserAsync(id);
await Assert.That(user).IsNotNull();
}
public class LazyDataProvider
{
public static IEnumerable<int> GetIds()
{
// Return only IDs during discovery
yield return 1;
yield return 2;
yield return 3;
}
}
Limit Matrix Test Combinations
// ❌ Bad: Exponential test explosion
[Test]
[Arguments(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)]
[Arguments("a", "b", "c", "d", "e")]
[Arguments(true, false)]
// Creates 10 × 5 × 2 = 100 tests!
// ✅ Good: Targeted test combinations
[Test]
[Arguments(1, "a", true)]
[Arguments(5, "c", false)]
[Arguments(10, "e", true)]
// Only 3 specific test cases
Test Execution Performance
Optimize Parallel Execution
Configure Appropriate Parallelism
// Set maximum parallel test execution
[assembly: MaxParallelTests(Environment.ProcessorCount)]
// Or use command line
// dotnet test --maximum-parallel-tests 8
Group Related Tests
// Tests in the same group run sequentially but different groups run in parallel
[ParallelGroup("DatabaseTests")]
public class UserRepositoryTests
{
// These tests share database resources
}
[ParallelGroup("DatabaseTests")]
public class OrderRepositoryTests
{
// These also share database resources
}
[ParallelGroup("ApiTests")]
public class ApiIntegrationTests
{
// These can run in parallel with database tests
}
Use Parallel Limiters Wisely
public class DatabaseConnectionLimit : IParallelLimit
{
public int Limit => 5; // Max 5 concurrent database connections
}
[ParallelLimiter<DatabaseConnectionLimit>]
public class DatabaseIntegrationTests
{
// All tests here respect the connection limit
}
Minimize Test Setup Overhead
Share Expensive Setup
// ❌ Bad: Expensive setup per test
public class ExpensiveTests
{
[Before(HookType.Test)]
public async Task SetupEachTest()
{
await StartDatabaseContainer();
await MigrateDatabase();
}
}
// ✅ Good: Share setup across tests
public class EfficientTests
{
private static DatabaseContainer? _container;
[Before(HookType.Class)]
public static async Task SetupOnce()
{
_container = await StartDatabaseContainer();
await MigrateDatabase();
}
[After(HookType.Class)]
public static async Task CleanupOnce()
{
if (_container != null)
{
await _container.DisposeAsync();
}
}
}
Use Lazy Initialization
public class PerformantTests
{
private static readonly Lazy<ExpensiveResource> _resource =
new(() => new ExpensiveResource(), LazyThreadSafetyMode.ExecutionAndPublication);
[Test]
public async Task TestUsingResource()
{
var resource = _resource.Value; // Only created on first access
await resource.DoSomethingAsync();
}
}
Optimize Assertions
Avoid Expensive Operations in Assertions
// ❌ Bad: Expensive operation in assertion
await Assert.That(await GetAllUsersFromDatabase())
.HasCount()
.EqualTo(1000);
// ✅ Good: Use efficient queries
var userCount = await GetUserCountFromDatabase();
await Assert.That(userCount).IsEqualTo(1000);
Use Early Exit Patterns
[Test]
public async Task EfficientValidation()
{
var result = await GetResultAsync();
// Quick checks first
if (result == null)
{
await Assert.That(result).IsNotNull();
return; // Exit early
}
// More expensive validations only if needed
await Assert.That(result.Items).HasCount().GreaterThan(0);
}
Memory Management
Dispose Resources Properly
public class MemoryEfficientTests : IAsyncDisposable
{
private readonly List<IDisposable> _disposables = new();
[Test]
public async Task TestWithResources()
{
var resource = new LargeResource();
_disposables.Add(resource);
// Use resource
await resource.ProcessAsync();
}
public async ValueTask DisposeAsync()
{
foreach (var disposable in _disposables)
{
disposable.Dispose();
}
_disposables.Clear();
// Force garbage collection if needed
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
Avoid Memory Leaks in Static Fields
// ❌ Bad: Static collections that grow indefinitely
public class LeakyTests
{
private static readonly List<TestResult> _allResults = new();
[After(HookType.Test)]
public void StoreResult()
{
_allResults.Add(GetCurrentResult()); // Memory leak!
}
}
// ✅ Good: Proper cleanup or bounded collections
public class EfficientTests
{
private static readonly Queue<TestResult> _recentResults = new();
private const int MaxResults = 100;
[After(HookType.Test)]
public void StoreResult()
{
_recentResults.Enqueue(GetCurrentResult());
while (_recentResults.Count > MaxResults)
{
_recentResults.Dequeue();
}
}
}
Use ValueTask for High-Frequency Operations
// For operations called many times, use ValueTask to reduce allocations
public async ValueTask<bool> FastCheckAsync(int id)
{
if (_cache.TryGetValue(id, out var cached))
{
return cached; // No allocation for cached path
}
var result = await LoadFromDatabaseAsync(id);
_cache[id] = result;
return result;
}
[Test]
[Arguments(1, 2, 3, 4, 5)] // Many invocations
public async Task HighFrequencyTest(int id)
{
var result = await FastCheckAsync(id);
await Assert.That(result).IsTrue();
}
I/O Performance
Batch Operations
// ❌ Bad: Individual operations
[Test]
public async Task SlowIOTest()
{
foreach (var id in Enumerable.Range(1, 100))
{
await SaveUserAsync(new User { Id = id });
}
}
// ✅ Good: Batch operations
[Test]
public async Task FastIOTest()
{
var users = Enumerable.Range(1, 100)
.Select(id => new User { Id = id })
.ToList();
await SaveUsersBatchAsync(users);
}
Use Async I/O
// ❌ Bad: Synchronous I/O
[Test]
public void SyncIOTest()
{
var content = File.ReadAllText("large-file.txt");
ProcessContent(content);
}
// ✅ Good: Asynchronous I/O
[Test]
public async Task AsyncIOTest()
{
var content = await File.ReadAllTextAsync("large-file.txt");
await ProcessContentAsync(content);
}
Cache File Contents
public class FileTestsWithCache
{
private static readonly ConcurrentDictionary<string, string> _fileCache = new();
private async Task<string> GetFileContentAsync(string path)
{
return await _fileCache.GetOrAddAsync(path,
async p => await File.ReadAllTextAsync(p));
}
[Test]
[Arguments("config1.json")]
[Arguments("config2.json")]
public async Task TestWithCachedFiles(string filename)
{
var content = await GetFileContentAsync(filename);
await Assert.That(content).IsNotEmpty();
}
}
Database Testing Performance
Use Transaction Rollback
public class FastDatabaseTests
{
[Test]
public async Task TransactionalTest()
{
using var transaction = await BeginTransactionAsync();
try
{
// Perform database operations
await CreateUserAsync("test@example.com");
// Verify
var user = await GetUserAsync("test@example.com");
await Assert.That(user).IsNotNull();
// Rollback instead of cleanup
await transaction.RollbackAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
}
Use In-Memory Databases for Unit Tests
public class InMemoryDatabaseTests
{
private DbContext CreateInMemoryContext()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
return new AppDbContext(options);
}
[Test]
public async Task FastDatabaseTest()
{
using var context = CreateInMemoryContext();
// Test runs entirely in memory
context.Users.Add(new User { Name = "Test" });
await context.SaveChangesAsync();
var count = await context.Users.CountAsync();
await Assert.That(count).IsEqualTo(1);
}
}
CI/CD Optimization
Split Test Suites
# Run fast unit tests first
dotnet test --filter "Category=Unit" --no-build
# Run slower integration tests separately
dotnet test --filter "Category=Integration" --no-build
# Run expensive E2E tests last
dotnet test --filter "Category=E2E" --no-build
Use Test Result Caching
<!-- Cache test results in CI -->
<PropertyGroup>
<TUnitCacheTestResults>true</TUnitCacheTestResults>
<TUnitTestResultsCachePath>$(Build.StagingDirectory)/testcache</TUnitTestResultsCachePath>
</PropertyGroup>
Fail Fast in CI
# Stop on first failure to save CI time
dotnet test --fail-fast
Monitoring and Profiling
Add Performance Logging
public class PerformanceAwareExecutor : ITestExecutor
{
private readonly ILogger<PerformanceAwareExecutor> _logger;
public async Task ExecuteAsync(TestContext context, Func<Task> testBody)
{
var stopwatch = Stopwatch.StartNew();
try
{
await testBody();
}
finally
{
stopwatch.Stop();
if (stopwatch.ElapsedMilliseconds > 1000)
{
_logger.LogWarning(
"Slow test detected: {TestName} took {ElapsedMs}ms",
context.TestDetails.TestName,
stopwatch.ElapsedMilliseconds);
}
}
}
}
Track Test Metrics
[After(HookType.Test)]
public static void RecordTestMetrics()
{
var context = TestContext.Current;
if (context?.Result != null)
{
TelemetryClient.TrackMetric(
"TestDuration",
context.Result.Duration.TotalMilliseconds,
new Dictionary<string, string>
{
["TestName"] = context.TestDetails.TestName,
["TestClass"] = context.TestDetails.TestClass,
["Result"] = context.Result.State.ToString()
});
}
}
Summary
Key performance principles:
- Optimize Discovery: Keep data sources lightweight and limit test combinations
- Parallelize Wisely: Use appropriate parallel limits and grouping
- Manage Resources: Dispose properly and avoid memory leaks
- Cache Aggressively: Cache expensive operations and file I/O
- Batch Operations: Group database and I/O operations
- Monitor Performance: Track and alert on slow tests
- Use AOT Mode: Enable AOT for best performance
- Fail Fast: Stop early on failures in CI
By following these practices, you can maintain a fast, efficient test suite that scales with your codebase.