AOT Compatibility and Generic Tests
TUnit's source generation mode provides compile-time safety and performance benefits, but requires specific patterns for advanced scenarios like generic tests and complex data sources.
Generic Test Instantiation
Generic test classes and methods require explicit type instantiation for AOT compatibility.
Generic Test Methods
Use the [GenerateGenericTest] attribute to specify which type combinations to generate:
using TUnit.Core;
namespace MyTestProject;
public class GenericTests
{
[Test]
[GenerateGenericTest(typeof(int), typeof(string))]
[GenerateGenericTest(typeof(long), typeof(bool))]
[GenerateGenericTest(typeof(double), typeof(char))]
public async Task GenericTestMethod<T1, T2>()
{
// Test logic using T1 and T2
var value1 = default(T1);
var value2 = default(T2);
await Assert.That(value1).IsNotNull().Or.IsEqualTo(default(T1));
await Assert.That(value2).IsNotNull().Or.IsEqualTo(default(T2));
}
}
Generic Test Classes
Apply [GenerateGenericTest] to the class to generate all test methods for specified types:
using TUnit.Core;
namespace MyTestProject;
[GenerateGenericTest(typeof(int))]
[GenerateGenericTest(typeof(string))]
[GenerateGenericTest(typeof(DateTime))]
public class GenericTestClass<T>
{
[Test]
public async Task TestDefaultValue()
{
var defaultValue = default(T);
// For reference types, default should be null
// For value types, default should be the type's default value
if (typeof(T).IsValueType)
{
await Assert.That(defaultValue).IsNotNull();
}
else
{
await Assert.That(defaultValue).IsNull();
}
}
[Test]
[Arguments("test data")]
public async Task TestWithGenericAndArguments(string input)
{
var value = default(T);
await Assert.That(input).IsEqualTo("test data");
// Can use both generic type T and regular parameters
}
}
AOT-Compatible Data Sources
Static Data Sources
Use static methods and properties for AOT-compatible data sources:
using TUnit.Core;
namespace MyTestProject;
public class AotDataSourceTests
{
// Static method data source - AOT compatible
[Test]
[MethodDataSource(nameof(GetTestData))]
public async Task TestWithStaticData(int value, string name)
{
await Assert.That(value).IsGreaterThan(0);
await Assert.That(name).IsNotEmpty();
}
public static IEnumerable<object[]> GetTestData()
{
yield return new object[] { 1, "first" };
yield return new object[] { 2, "second" };
yield return new object[] { 3, "third" };
}
// Static property data source - AOT compatible
[Test]
[MethodDataSource(nameof(PropertyTestData))]
public async Task TestWithPropertyData(bool flag, double number)
{
await Assert.That(flag).IsTrue().Or.IsFalse(); // Either is valid
await Assert.That(number).IsGreaterThanOrEqualTo(0.0);
}
public static IEnumerable<object[]> PropertyTestData => new[]
{
new object[] { true, 1.5 },
new object[] { false, 2.7 },
new object[] { true, 0.0 }
};
}
Async Data Sources with Cancellation
AOT mode supports async data sources with proper cancellation token handling:
using TUnit.Core;
using System.Runtime.CompilerServices;
namespace MyTestProject;
public class AsyncDataSourceTests
{
[Test]
[MethodDataSource(nameof(GetAsyncTestData))]
public async Task TestWithAsyncData(int id, string data)
{
await Assert.That(id).IsGreaterThan(0);
await Assert.That(data).StartsWith("data");
}
public static async IAsyncEnumerable<object[]> GetAsyncTestData(
[EnumeratorCancellation] CancellationToken ct = default)
{
for (int i = 1; i <= 3; i++)
{
ct.ThrowIfCancellationRequested();
// Simulate async work
await Task.Delay(10, ct);
yield return new object[] { i, $"data_{i}" };
}
}
}
Advanced Property Injection
Service Provider Integration
AOT mode includes a built-in service provider for dependency injection:
using TUnit.Core;
using TUnit.Core.Services;
namespace MyTestProject;
public class ServiceInjectionTests
{
[ClassDataSource<DatabaseService>(Shared = SharedType.PerTestSession)]
public required DatabaseService Database { get; init; }
[ClassDataSource<LoggingService>]
public required LoggingService Logger { get; init; }
[Test]
public async Task TestWithInjectedServices()
{
// Services are automatically injected before test execution
await Assert.That(Database).IsNotNull();
await Assert.That(Logger).IsNotNull();
var result = await Database.QueryAsync("SELECT 1");
Logger.Log($"Query result: {result}");
await Assert.That(result).IsEqualTo(1);
}
}
// Example service classes
public class DatabaseService
{
public async Task<int> QueryAsync(string sql)
{
await Task.Delay(1); // Simulate async database call
return 1;
}
}
public class LoggingService
{
public void Log(string message)
{
Console.WriteLine($"[LOG] {message}");
}
}
Async Property Initialization
Properties can implement IAsyncInitializable for complex setup:
using TUnit.Core;
namespace MyTestProject;
public class AsyncInitializationTests
{
[ClassDataSource<AsyncContainer>]
public required AsyncContainer Container { get; init; }
[Test]
public async Task TestWithAsyncInitializedProperty()
{
// Container.InitializeAsync() is called automatically before test
await Assert.That(Container.IsInitialized).IsTrue();
await Assert.That(Container.ConnectionString).IsNotEmpty();
}
}
public class AsyncContainer : IAsyncInitializable, IAsyncDisposable
{
public bool IsInitialized { get; private set; }
public string ConnectionString { get; private set; } = "";
public async Task InitializeAsync()
{
// Simulate async initialization
await Task.Delay(10);
ConnectionString = "Server=localhost;Database=test";
IsInitialized = true;
}
public async ValueTask DisposeAsync()
{
// Cleanup is called automatically after test
await Task.Delay(1);
IsInitialized = false;
ConnectionString = "";
}
}
Compile-Time Diagnostics
AOT mode provides helpful compile-time diagnostics for common issues:
Generic Test Diagnostics
// ❌ This will generate TUnit0058 error
[Test]
public async Task GenericTest<T>() // Missing [GenerateGenericTest]
{
var value = default(T);
await Assert.That(value).IsNotNull().Or.IsNull();
}
// ✅ Correct usage
[Test]
[GenerateGenericTest(typeof(int))]
[GenerateGenericTest(typeof(string))]
public async Task GenericTest<T>()
{
var value = default(T);
await Assert.That(value).IsNotNull().Or.IsNull();
}
Data Source Diagnostics
public class DataSourceDiagnostics
{
// ❌ This will generate TUnit0059 error - dynamic data source
[Test]
[MethodDataSource(nameof(GetDynamicData))]
public async Task TestWithDynamicDataSource(object value)
{
await Assert.That(value).IsNotNull();
}
public IEnumerable<object[]> GetDynamicData()
{
// This method uses reflection internally - not AOT compatible
return SomeReflectionBasedDataGenerator.GetData();
}
// ✅ Use static, compile-time known data sources
[Test]
[MethodDataSource(nameof(GetStaticData))]
public async Task TestWithStaticDataSource(string value)
{
await Assert.That(value).IsNotNull();
}
public static IEnumerable<object[]> GetStaticData()
{
yield return new object[] { "static1" };
yield return new object[] { "static2" };
}
}
Benefits
The source generation mode provides several advantages:
- Improved test execution performance compared to reflection-based approaches
- Zero runtime type introspection - all types resolved at compile time
- Reduced memory allocations through strongly-typed delegates
- Better code optimization from the compiler and runtime
Configuration Reference
Configure AOT behavior through your .editorconfig file:
# TUnit Configuration
# The only configurable option for troubleshooting:
tunit.enable_verbose_diagnostics = false # Verbose diagnostics (default: false)
These settings help balance compilation time, binary size, and functionality based on your project's needs.