Generic Attributes
TUnit provides generic versions of several attributes that offer enhanced type safety and better IDE support. These attributes allow you to specify types at compile time, reducing errors and improving code maintainability.
Generic Test Attributesâ
MethodDataSourceAttribute<T>â
The generic version of MethodDataSource provides type safety for the class containing the data source method.
public class TestDataProviders
{
public static IEnumerable<(int, int, int)> AdditionTestCases()
{
yield return (1, 2, 3);
yield return (5, 5, 10);
yield return (-1, 1, 0);
}
}
public class CalculatorTests
{
[Test]
[MethodDataSource<TestDataProviders>(nameof(TestDataProviders.AdditionTestCases))]
public void Add_ShouldReturnCorrectSum(int a, int b, int expected)
{
var result = Calculator.Add(a, b);
Assert.That(result).IsEqualTo(expected);
}
}
Benefits over non-generic version:
- Compile-time type checking
- IDE refactoring support
- Prevents typos in class names
ClassDataSourceAttribute<T>â
The generic version ensures type safety when referencing data source classes.
public class UserTestData : IEnumerable<User>
{
public IEnumerator<User> GetEnumerator()
{
yield return new User { Id = 1, Name = "Alice" };
yield return new User { Id = 2, Name = "Bob" };
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public class UserTests
{
[Test]
[ClassDataSource<UserTestData>]
public async Task ValidateUser_ShouldPass(User user)
{
var isValid = await UserValidator.ValidateAsync(user);
await Assert.That(isValid).IsTrue();
}
}
DependsOnAttribute<T>â
The generic DependsOn attribute provides type-safe test dependency declarations.
public class OrderProcessingTests
{
[Test]
public async Task CreateOrder()
{
// Create order logic
}
[Test]
[DependsOn<OrderProcessingTests>(nameof(CreateOrder))]
public async Task ProcessPayment()
{
// This test depends on CreateOrder from the same class
}
}
public class ShippingTests
{
[Test]
[DependsOn<OrderProcessingTests>(nameof(OrderProcessingTests.ProcessPayment))]
public async Task ShipOrder()
{
// This test depends on ProcessPayment from another class
}
}
Generic Data Source Attributesâ
DataSourceGeneratorAttribute<T>â
Create strongly-typed data source generators:
public abstract class DataSourceGeneratorAttribute<T> : Attribute
{
public abstract IEnumerable<T> GenerateData();
}
// Custom implementation
public class RandomNumbersAttribute : DataSourceGeneratorAttribute<int>
{
private readonly int _count;
private readonly int _min;
private readonly int _max;
public RandomNumbersAttribute(int count, int min = 0, int max = 100)
{
_count = count;
_min = min;
_max = max;
}
public override IEnumerable<int> GenerateData()
{
var random = new Random();
for (int i = 0; i < _count; i++)
{
yield return random.Next(_min, _max);
}
}
}
// Usage
[Test]
[RandomNumbers(5, min: 1, max: 10)]
public void TestWithRandomNumbers(int number)
{
Assert.That(number).IsBetween(1, 10);
}
AsyncDataSourceGeneratorAttribute<T>â
For asynchronous data generation:
public abstract class AsyncDataSourceGeneratorAttribute<T> : Attribute
{
public abstract Task<IEnumerable<T>> GenerateDataAsync();
}
// Custom implementation
public class DatabaseUsersAttribute : AsyncDataSourceGeneratorAttribute<User>
{
private readonly string _role;
public DatabaseUsersAttribute(string role)
{
_role = role;
}
public override async Task<IEnumerable<User>> GenerateDataAsync()
{
using var db = new DatabaseContext();
return await db.Users
.Where(u => u.Role == _role)
.ToListAsync();
}
}
// Usage
[Test]
[DatabaseUsers("Admin")]
public async Task AdminUser_ShouldHaveFullPermissions(User adminUser)
{
var permissions = await GetUserPermissions(adminUser);
await Assert.That(permissions).Contains(Permission.FullAccess);
}
TypedDataSourceAttribute<T>â
Base class for creating custom typed data sources:
public abstract class TypedDataSourceAttribute<T> : DataSourceAttribute
{
public abstract IEnumerable<T> GetData();
}
// Implementation example
public class FibonacciDataAttribute : TypedDataSourceAttribute<int>
{
private readonly int _count;
public FibonacciDataAttribute(int count)
{
_count = count;
}
public override IEnumerable<int> GetData()
{
int a = 0, b = 1;
yield return a;
if (_count > 1) yield return b;
for (int i = 2; i < _count; i++)
{
int temp = a + b;
yield return temp;
a = b;
b = temp;
}
}
}
// Usage
[Test]
[FibonacciData(7)]
public void TestFibonacciNumber(int fibNumber)
{
// Test with Fibonacci sequence: 0, 1, 1, 2, 3, 5, 8
Assert.That(fibNumber).IsGreaterThanOrEqualTo(0);
}
Complex Generic Scenariosâ
Combining Multiple Generic Attributesâ
public interface ITestScenario<TInput, TExpected>
{
TInput Input { get; }
TExpected Expected { get; }
}
public class CalculationScenario : ITestScenario<(int, int), int>
{
public (int, int) Input { get; set; }
public int Expected { get; set; }
}
public class ScenarioDataSource<TScenario> : TypedDataSourceAttribute<TScenario>
where TScenario : ITestScenario<(int, int), int>, new()
{
public override IEnumerable<TScenario> GetData()
{
yield return new TScenario { Input = (1, 2), Expected = 3 };
yield return new TScenario { Input = (5, 5), Expected = 10 };
}
}
[Test]
[ScenarioDataSource<CalculationScenario>]
public void TestCalculation(CalculationScenario scenario)
{
var (a, b) = scenario.Input;
var result = Calculator.Add(a, b);
Assert.That(result).IsEqualTo(scenario.Expected);
}
Generic Test Base Classesâ
â ī¸ Important Limitation: C# does not allow generic type parameters to be used as attribute arguments. This is a known language limitation (see dotnet/csharplang#124).
The following code WILL NOT COMPILE due to error CS8968:
// â This does NOT work - CS8968 error
public abstract class EntityTestBase<TEntity, TId>
where TEntity : IEntity<TId>
{
[Test]
[MethodDataSource<EntityTestBase<TEntity, TId>>(nameof(GetTestIds))] // â Error!
public async Task Entity_ShouldBeRetrievable(TId id) { }
}
Workaround 1: Use InstanceMethodDataSourceâ
The recommended approach is to use InstanceMethodDataSource instead:
public abstract class EntityTestBase<TEntity, TId>
where TEntity : IEntity<TId>
where TId : IEquatable<TId>
{
protected abstract TEntity CreateEntity(TId id);
protected abstract Task<TEntity> GetEntityAsync(TId id);
// â
This works - instance method data source
[Test]
[InstanceMethodDataSource(nameof(GetTestIds))]
public async Task Entity_ShouldBeRetrievable(TId id)
{
var entity = CreateEntity(id);
await SaveEntityAsync(entity);
var retrieved = await GetEntityAsync(id);
await Assert.That(retrieved.Id).IsEqualTo(id);
}
// Instance method (not static)
public IEnumerable<TId> GetTestIds()
{
return GetTestIdsCore();
}
protected abstract IEnumerable<TId> GetTestIdsCore();
}
public class UserEntityTests : EntityTestBase<User, Guid>
{
protected override User CreateEntity(Guid id) =>
new User { Id = id, Name = "Test User" };
protected override Task<User> GetEntityAsync(Guid id) =>
UserRepository.GetByIdAsync(id);
protected override IEnumerable<Guid> GetTestIdsCore()
{
yield return Guid.NewGuid();
yield return Guid.NewGuid();
}
}
Workaround 2: Create Concrete Base Classesâ
For a limited set of types, create non-generic derived classes:
// Base generic class (no data source attributes using generics)
public abstract class EntityTestBase<TEntity, TId>
where TEntity : IEntity<TId>
where TId : IEquatable<TId>
{
protected abstract TEntity CreateEntity(TId id);
protected abstract Task<TEntity> GetEntityAsync(TId id);
protected async Task Entity_ShouldBeRetrievable(TId id)
{
var entity = CreateEntity(id);
await SaveEntityAsync(entity);
var retrieved = await GetEntityAsync(id);
await Assert.That(retrieved.Id).IsEqualTo(id);
}
}
// Concrete base class for Guid-based entities
public abstract class GuidEntityTestBase<TEntity> : EntityTestBase<TEntity, Guid>
where TEntity : IEntity<Guid>
{
[Test]
[MethodDataSource<GuidEntityTestBase<TEntity>>(nameof(GetTestIds))]
public async Task TestEntity(Guid id)
{
await Entity_ShouldBeRetrievable(id);
}
public static IEnumerable<Guid> GetTestIds()
{
yield return Guid.NewGuid();
yield return Guid.NewGuid();
}
}
// Your test class
public class UserEntityTests : GuidEntityTestBase<User>
{
protected override User CreateEntity(Guid id) =>
new User { Id = id, Name = "Test User" };
protected override Task<User> GetEntityAsync(Guid id) =>
UserRepository.GetByIdAsync(id);
}
AOT Compatibilityâ
Generic attributes work well with AOT compilation, but there are some considerations:
DynamicallyAccessedMembersâ
When creating generic attributes that use reflection, add appropriate attributes:
public class ReflectiveDataSource<[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicConstructors |
DynamicallyAccessedMemberTypes.PublicProperties)] T>
: TypedDataSourceAttribute<T> where T : new()
{
public override IEnumerable<T> GetData()
{
var type = typeof(T);
var properties = type.GetProperties();
// Create instances with different property values
foreach (var prop in properties)
{
var instance = new T();
// Set property values...
yield return instance;
}
}
}
Generic Constraints for AOTâ
Use constraints to ensure AOT compatibility:
public class SerializableDataSource<T> : TypedDataSourceAttribute<T>
where T : IJsonSerializable<T> // Ensures T can be serialized
{
private readonly string _jsonFile;
public SerializableDataSource(string jsonFile)
{
_jsonFile = jsonFile;
}
public override IEnumerable<T> GetData()
{
var json = File.ReadAllText(_jsonFile);
var items = JsonSerializer.Deserialize<List<T>>(json);
return items ?? Enumerable.Empty<T>();
}
}
Best Practicesâ
1. Use Generic Attributes for Type Safetyâ
// â Non-generic - prone to errors
[MethodDataSource(typeof(DataProvider), "GetData")]
// â
Generic - compile-time safety
[MethodDataSource<DataProvider>(nameof(DataProvider.GetData))]
2. Leverage Constraintsâ
public class ValidatableDataSource<T> : TypedDataSourceAttribute<T>
where T : IValidatable
{
public override IEnumerable<T> GetData()
{
// Only return valid instances
return GenerateInstances().Where(x => x.IsValid());
}
}
3. Create Reusable Generic Base Attributesâ
public abstract class JsonFileDataSource<T> : TypedDataSourceAttribute<T>
{
protected abstract string FilePath { get; }
public override IEnumerable<T> GetData()
{
var json = File.ReadAllText(FilePath);
return JsonSerializer.Deserialize<List<T>>(json)
?? Enumerable.Empty<T>();
}
}
public class UserJsonDataSource : JsonFileDataSource<User>
{
protected override string FilePath => "TestData/users.json";
}
4. Document Generic Type Parametersâ
/// <summary>
/// Provides test data from a CSV file
/// </summary>
/// <typeparam name="T">The type to deserialize CSV rows into.
/// Must have a parameterless constructor.</typeparam>
public class CsvDataSource<T> : TypedDataSourceAttribute<T>
where T : new()
{
// Implementation
}
Common Patternsâ
Factory Pattern with Genericsâ
public class EntityFactory<T> where T : IEntity, new()
{
public static IEnumerable<T> CreateTestEntities(int count)
{
for (int i = 0; i < count; i++)
{
yield return new T
{
Id = i,
CreatedAt = DateTime.UtcNow
};
}
}
}
public class FactoryDataSource<T> : TypedDataSourceAttribute<T>
where T : IEntity, new()
{
private readonly int _count;
public FactoryDataSource(int count = 3)
{
_count = count;
}
public override IEnumerable<T> GetData()
{
return EntityFactory<T>.CreateTestEntities(_count);
}
}
// Usage
[Test]
[FactoryDataSource<Product>(5)]
public async Task TestProductEntity(Product product)
{
await Assert.That(product.Id).IsGreaterThanOrEqualTo(0);
}
Builder Pattern with Genericsâ
public abstract class TestDataBuilder<T> : TypedDataSourceAttribute<T>
{
protected abstract T BuildDefault();
protected abstract T BuildInvalid();
protected abstract T BuildEdgeCase();
public override IEnumerable<T> GetData()
{
yield return BuildDefault();
yield return BuildInvalid();
yield return BuildEdgeCase();
}
}
public class UserDataBuilder : TestDataBuilder<User>
{
protected override User BuildDefault() =>
new User { Id = 1, Name = "John", Age = 30 };
protected override User BuildInvalid() =>
new User { Id = -1, Name = "", Age = -5 };
protected override User BuildEdgeCase() =>
new User { Id = int.MaxValue, Name = new string('a', 1000), Age = 150 };
}
Summaryâ
Generic attributes in TUnit provide:
- Type Safety: Compile-time checking prevents runtime errors
- Better IDE Support: Refactoring and navigation work correctly
- Cleaner Code: No magic strings or typeof expressions
- AOT Compatibility: Work well with ahead-of-time compilation
- Reusability: Easy to create generic base attributes for common patterns
Use generic attributes whenever possible to improve code quality and maintainability in your test suites.