Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions TUnit.Core/Attributes/BaseTestAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
namespace TUnit.Core;

// Any new test type attributes should inherit from this
// This ensures we have a location of the test provided by the compiler
// Using [CallerLineNumber] [CallerFilePath]
/// <summary>
/// Base class for test method attributes. Automatically captures the source file path and line number
/// where the test is defined, using compiler services.
/// </summary>
/// <remarks>
/// Inherit from this class to create custom test type attributes. The <see cref="File"/> and <see cref="Line"/>
/// properties are populated automatically by the compiler via <c>[CallerFilePath]</c> and <c>[CallerLineNumber]</c>.
/// </remarks>
[AttributeUsage(AttributeTargets.Method)]
public abstract class BaseTestAttribute : TUnitAttribute
{
/// <summary>
/// Gets the source file path where the test is defined.
/// </summary>
public readonly string File;

/// <summary>
/// Gets the line number in the source file where the test is defined.
/// </summary>
public readonly int Line;

internal BaseTestAttribute(string file, int line)
Expand Down
43 changes: 43 additions & 0 deletions TUnit.Core/Attributes/ClassConstructorSourceAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,63 @@

namespace TUnit.Core;

/// <summary>
/// Specifies a custom <see cref="IClassConstructor"/> to use when creating test class instances.
/// This enables dependency injection and custom object creation for test classes.
/// </summary>
/// <remarks>
/// <para>
/// Can be applied at the class level (affecting only that class) or at the assembly level
/// (affecting all test classes in the assembly).
/// </para>
/// <para>
/// The specified type must implement <see cref="IClassConstructor"/> and have a parameterless constructor.
/// For strongly-typed usage, prefer <see cref="ClassConstructorAttribute{T}"/>.
/// </para>
/// </remarks>
/// <example>
/// <code>
/// [ClassConstructor&lt;DependencyInjectionClassConstructor&gt;]
/// public class MyTests
/// {
/// private readonly IMyService _service;
///
/// public MyTests(IMyService service)
/// {
/// _service = service;
/// }
///
/// [Test]
/// public void TestWithInjectedDependency() { }
/// }
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class)]
public class ClassConstructorAttribute : TUnitAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="ClassConstructorAttribute"/> class.
/// </summary>
/// <param name="classConstructorType">The type that implements <see cref="IClassConstructor"/>.</param>
public ClassConstructorAttribute(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)]
Type classConstructorType)
{
ClassConstructorType = classConstructorType;
}

/// <summary>
/// Gets or sets the type that implements <see cref="IClassConstructor"/> and is used to create test class instances.
/// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)]
public Type ClassConstructorType { get; init; }
}

/// <summary>
/// Specifies a custom <see cref="IClassConstructor"/> to use when creating test class instances.
/// Generic version that provides compile-time type safety.
/// </summary>
/// <typeparam name="T">The type that implements <see cref="IClassConstructor"/>.</typeparam>
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class)]
public sealed class ClassConstructorAttribute<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)] T>()
Expand Down
4 changes: 4 additions & 0 deletions TUnit.Core/Attributes/DynamicTestBuilderAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@

namespace TUnit.Core;

/// <summary>
/// Marks a method as a dynamic test builder that programmatically generates test cases at runtime.
/// Methods decorated with this attribute can yield test definitions dynamically.
/// </summary>
public class DynamicTestBuilderAttribute([CallerFilePath] string file = "", [CallerLineNumber] int line = 0) : BaseTestAttribute(file, line);
4 changes: 4 additions & 0 deletions TUnit.Core/Attributes/TUnitAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
namespace TUnit.Core;

/// <summary>
/// Base class for all TUnit framework attributes.
/// This class cannot be instantiated directly; use one of the derived attribute types.
/// </summary>
public class TUnitAttribute : Attribute
{
internal TUnitAttribute()
Expand Down
28 changes: 28 additions & 0 deletions TUnit.Core/Attributes/TestData/ArgumentsAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,15 @@ namespace TUnit.Core;
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)]
public sealed class ArgumentsAttribute : Attribute, IDataSourceAttribute, ITestRegisteredEventReceiver
{
/// <summary>
/// Gets the array of argument values to pass to the test method.
/// </summary>
public object?[] Values { get; }

/// <summary>
/// Gets or sets a reason to skip this specific test case.
/// When set, the test case will be skipped with the given reason.
/// </summary>
public string? Skip { get; set; }

/// <summary>
Expand All @@ -60,6 +67,10 @@ public sealed class ArgumentsAttribute : Attribute, IDataSourceAttribute, ITestR
/// <inheritdoc />
public bool SkipIfEmpty { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="ArgumentsAttribute"/> class with the specified test argument values.
/// </summary>
/// <param name="values">The argument values to pass to the test method. Pass <c>null</c> for a single null argument.</param>
public ArgumentsAttribute(params object?[]? values)
{
if (values == null)
Expand Down Expand Up @@ -112,9 +123,26 @@ public ValueTask OnTestRegistered(TestRegisteredContext context)
public int Order => 0;
}

/// <summary>
/// Provides a strongly-typed inline value for a parameterized test with a single parameter.
/// </summary>
/// <typeparam name="T">The type of the test parameter.</typeparam>
/// <param name="value">The value to pass to the test method.</param>
/// <example>
/// <code>
/// [Test]
/// [Arguments&lt;string&gt;("hello")]
/// [Arguments&lt;string&gt;("world")]
/// public void TestWithString(string input) { }
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)]
public sealed class ArgumentsAttribute<T>(T value) : TypedDataSourceAttribute<T>, ITestRegisteredEventReceiver
{
/// <summary>
/// Gets or sets a reason to skip this specific test case.
/// When set, the test case will be skipped with the given reason.
/// </summary>
public string? Skip { get; set; }

/// <summary>
Expand Down
57 changes: 57 additions & 0 deletions TUnit.Core/Attributes/TestData/ClassDataSourceAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,33 @@

namespace TUnit.Core;

/// <summary>
/// Provides test data by creating instances of one or more specified types.
/// The instances are created using their constructors and can optionally be shared across tests.
/// </summary>
/// <remarks>
/// <para>
/// Use this attribute to inject class instances as test method parameters or constructor arguments.
/// The attribute supports sharing instances across tests via the <see cref="Shared"/> property and
/// keyed sharing via the <see cref="Keys"/> property.
/// </para>
/// <para>
/// For strongly-typed single-parameter usage, prefer the generic version <see cref="ClassDataSourceAttribute{T}"/>.
/// </para>
/// </remarks>
/// <example>
/// <code>
/// // Create a new instance for each test
/// [Test]
/// [ClassDataSource(typeof(MyService))]
/// public void TestWithService(MyService service) { }
///
/// // Share the instance across all tests in the class
/// [Test]
/// [ClassDataSource(typeof(MyService), Shared = [SharedType.PerClass])]
/// public void TestWithSharedService(MyService service) { }
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)]
public sealed class ClassDataSourceAttribute : UntypedDataSourceGeneratorAttribute
{
Expand Down Expand Up @@ -74,7 +101,16 @@ public ClassDataSourceAttribute(params Type[] types)
_types = types;
}

/// <summary>
/// Gets or sets how instances are shared across tests, one per type parameter.
/// Defaults to <see cref="SharedType.None"/> (a new instance per test).
/// </summary>
public SharedType[] Shared { get; set; } = [SharedType.None];

/// <summary>
/// Gets or sets the sharing keys, one per type parameter.
/// Used when <see cref="Shared"/> is set to <see cref="SharedType.Keyed"/> to identify shared instances.
/// </summary>
public string[] Keys { get; set; } = [];

[UnconditionalSuppressMessage("Trimming", "IL2062:The parameter of method has a DynamicallyAccessedMembersAttribute, but the value passed to it can not be statically analyzed.",
Expand Down Expand Up @@ -121,6 +157,27 @@ public ClassDataSourceAttribute(params Type[] types)

}

/// <summary>
/// Provides test data by creating an instance of <typeparamref name="T"/>.
/// The instance is created using its constructor and can optionally be shared across tests.
/// </summary>
/// <typeparam name="T">The type to instantiate as test data.</typeparam>
/// <remarks>
/// <para>
/// Use the <see cref="Shared"/> property to control instance sharing:
/// <see cref="SharedType.None"/> (default) creates a new instance per test,
/// <see cref="SharedType.PerClass"/> shares within the test class,
/// <see cref="SharedType.PerAssembly"/> shares across the assembly,
/// <see cref="SharedType.Keyed"/> shares by a specified <see cref="Key"/>.
/// </para>
/// </remarks>
/// <example>
/// <code>
/// [Test]
/// [ClassDataSource&lt;MyService&gt;(Shared = SharedType.PerClass)]
/// public void TestWithService(MyService service) { }
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)]
public sealed class ClassDataSourceAttribute<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] T>
: DataSourceGeneratorAttribute<T>
Expand Down
10 changes: 10 additions & 0 deletions TUnit.Core/Attributes/TestData/IDataSourceAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
namespace TUnit.Core;

/// <summary>
/// Defines a data source that provides test data for parameterized tests.
/// Implement this interface to create custom data source attributes.
/// </summary>
public interface IDataSourceAttribute
{
/// <summary>
/// Asynchronously generates data rows for parameterized tests.
/// Each yielded function, when invoked, produces one set of arguments for a test invocation.
/// </summary>
/// <param name="dataGeneratorMetadata">Metadata about the test and parameters being generated.</param>
/// <returns>An async enumerable of factory functions that produce test data rows.</returns>
public IAsyncEnumerable<Func<Task<object?[]?>>> GetDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata);

/// <summary>
Expand Down
30 changes: 30 additions & 0 deletions TUnit.Core/Attributes/TestData/MatrixDataSourceAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,36 @@

namespace TUnit.Core;

/// <summary>
/// Generates test cases from all combinations (Cartesian product) of parameter values.
/// </summary>
/// <remarks>
/// <para>
/// For boolean parameters, all values (<c>true</c>, <c>false</c>) are generated automatically.
/// For enum parameters, all defined enum values are generated automatically.
/// For other types, use <c>[Matrix(...)]</c> on individual parameters to specify the values.
/// </para>
/// <para>
/// Use <see cref="MatrixExclusionAttribute"/> on the test method to exclude specific combinations.
/// </para>
/// </remarks>
/// <example>
/// <code>
/// [Test, MatrixDataSource]
/// public void TestAllCombinations(
/// [Matrix(1, 2, 3)] int x,
/// [Matrix("a", "b")] string y)
/// {
/// // Generates 6 test cases: (1,"a"), (1,"b"), (2,"a"), (2,"b"), (3,"a"), (3,"b")
/// }
///
/// [Test, MatrixDataSource]
/// public void TestWithEnum(bool enabled, MyEnum mode)
/// {
/// // Automatically generates all bool x enum combinations
/// }
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public sealed class MatrixDataSourceAttribute : UntypedDataSourceGeneratorAttribute, IAccessesInstanceData
{
Expand Down
60 changes: 60 additions & 0 deletions TUnit.Core/Attributes/TestData/MethodDataSourceAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,57 @@

namespace TUnit.Core;

/// <summary>
/// Provides test data from a method, property, or field on the specified type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type containing the data source member.</typeparam>
/// <param name="methodNameProvidingDataSource">The name of the method, property, or field that provides the test data.</param>
/// <example>
/// <code>
/// [Test]
/// [MethodDataSource&lt;TestDataProvider&gt;(nameof(TestDataProvider.GetTestCases))]
/// public void MyTest(int input, string expected) { }
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)]
public class MethodDataSourceAttribute<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)]
T>(string methodNameProvidingDataSource)
: MethodDataSourceAttribute(typeof(T), methodNameProvidingDataSource);

/// <summary>
/// Provides test data from a method, property, or field in the test class or a specified type.
/// </summary>
/// <remarks>
/// <para>
/// The data source can be a method, property, or field that returns test data.
/// Supported return types include single values, <see cref="IEnumerable{T}"/>, <see cref="IAsyncEnumerable{T}"/>,
/// <see cref="Task{T}"/>, tuples, and arrays.
/// </para>
/// <para>
/// When no class type is specified, the data source is looked up in the test class itself.
/// Both static and instance members are supported.
/// </para>
/// </remarks>
/// <example>
/// <code>
/// // Using a method in the same class
/// [Test]
/// [MethodDataSource(nameof(GetTestData))]
/// public void MyTest(int value, string name) { }
///
/// public static IEnumerable&lt;(int, string)&gt; GetTestData()
/// {
/// yield return (1, "one");
/// yield return (2, "two");
/// }
///
/// // Using a method in another class
/// [Test]
/// [MethodDataSource(typeof(SharedData), nameof(SharedData.GetValues))]
/// public void MyTest(string value) { }
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)]
public class MethodDataSourceAttribute : Attribute, IDataSourceAttribute
{
Expand All @@ -20,12 +65,27 @@ public class MethodDataSourceAttribute : Attribute, IDataSourceAttribute
| System.Reflection.BindingFlags.Instance
| System.Reflection.BindingFlags.FlattenHierarchy;

/// <summary>
/// Gets the type containing the data source member, or <c>null</c> if the data source is in the test class itself.
/// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)]
public Type? ClassProvidingDataSource { get; }

/// <summary>
/// Gets the name of the method, property, or field that provides the test data.
/// </summary>
public string MethodNameProvidingDataSource { get; }

/// <summary>
/// Gets or sets an AOT-safe factory function for providing test data programmatically.
/// When set, this factory is used instead of reflection-based member lookup.
/// </summary>
public Func<DataGeneratorMetadata, IAsyncEnumerable<Func<Task<object?[]?>>>>? Factory { get; set; }

/// <summary>
/// Gets or sets the arguments to pass to the data source method.
/// Use this when the data source method requires parameters.
/// </summary>
public object?[] Arguments { get; set; } = [];

/// <inheritdoc />
Expand Down
Loading
Loading