From f9856613027af3f58350f81d08d3c76a24c07460 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Thu, 19 Feb 2026 04:04:01 +0000
Subject: [PATCH 1/4] feat: add ITUnitPlugin interface and improve XML docs for
extension points
Add a unified ITUnitPlugin interface that bundles the common test lifecycle
event receiver interfaces (ITestRegisteredEventReceiver, ITestDiscoveryEventReceiver,
ITestStartEventReceiver, ITestEndEventReceiver, ITestSkippedEventReceiver,
ITestRetryEventReceiver) with default no-op implementations on .NET 8.0+.
This makes it easier for third-party libraries to create plugins that hook
into multiple lifecycle events without implementing each interface separately.
Also add comprehensive XML documentation to previously undocumented or
minimally documented extension interfaces: ITestRetryEventReceiver,
ITestSkippedEventReceiver, IHookRegisteredEventReceiver, IHookExecutor,
ITestFinder, and improve docs for ITestStartEventReceiver,
ITestEndEventReceiver, ITestRegisteredEventReceiver, and
ITestDiscoveryEventReceiver with usage examples and lifecycle ordering.
---
TUnit.Core/Interfaces/IHookExecutor.cs | 97 +++++++++++++++++++
.../IHookRegisteredEventReceiver.cs | 26 ++++-
TUnit.Core/Interfaces/ITUnitPlugin.cs | 87 +++++++++++++++++
.../Interfaces/ITestDiscoveryEventReceiver.cs | 44 ++++++++-
.../Interfaces/ITestEndEventReceiver.cs | 26 ++++-
TUnit.Core/Interfaces/ITestFinder.cs | 28 ++++++
.../ITestRegisteredEventReceiver.cs | 21 +++-
.../Interfaces/ITestRetryEventReceiver.cs | 37 +++++++
.../Interfaces/ITestSkippedEventReceiver.cs | 37 +++++++
.../Interfaces/ITestStartEventReceiver.cs | 50 +++++++++-
10 files changed, 443 insertions(+), 10 deletions(-)
create mode 100644 TUnit.Core/Interfaces/ITUnitPlugin.cs
diff --git a/TUnit.Core/Interfaces/IHookExecutor.cs b/TUnit.Core/Interfaces/IHookExecutor.cs
index 6d3d1c8e81..d48e70fc2a 100644
--- a/TUnit.Core/Interfaces/IHookExecutor.cs
+++ b/TUnit.Core/Interfaces/IHookExecutor.cs
@@ -1,16 +1,113 @@
namespace TUnit.Core.Interfaces;
+///
+/// Defines a mechanism for executing lifecycle hooks within the TUnit test framework.
+///
+///
+///
+/// The interface provides a way to customize how lifecycle hooks
+/// (such as [Before(Test)], [After(Class)], etc.) are executed. Implementers
+/// can control the execution environment, threading model, or synchronization context
+/// for hook execution, similar to how controls test execution.
+///
+///
+/// This is particularly useful when hooks need to run on a specific thread (e.g., STA thread
+/// for UI testing), within a specific synchronization context, or with custom error handling.
+///
+///
+/// Hook executors can be specified using the [HookExecutor<T>] attribute at the
+/// assembly, class, or method level.
+///
+///
public interface IHookExecutor
{
+ ///
+ /// Executes a "before test discovery" hook.
+ ///
+ /// Metadata about the hook method being executed.
+ /// The context for the test discovery phase.
+ /// The hook body to execute.
+ /// A representing the asynchronous operation.
ValueTask ExecuteBeforeTestDiscoveryHook(MethodMetadata hookMethodInfo, BeforeTestDiscoveryContext context, Func action);
+
+ ///
+ /// Executes a "before test session" hook.
+ ///
+ /// Metadata about the hook method being executed.
+ /// The test session context.
+ /// The hook body to execute.
+ /// A representing the asynchronous operation.
ValueTask ExecuteBeforeTestSessionHook(MethodMetadata hookMethodInfo, TestSessionContext context, Func action);
+
+ ///
+ /// Executes a "before assembly" hook.
+ ///
+ /// Metadata about the hook method being executed.
+ /// The assembly hook context.
+ /// The hook body to execute.
+ /// A representing the asynchronous operation.
ValueTask ExecuteBeforeAssemblyHook(MethodMetadata hookMethodInfo, AssemblyHookContext context, Func action);
+
+ ///
+ /// Executes a "before class" hook.
+ ///
+ /// Metadata about the hook method being executed.
+ /// The class hook context.
+ /// The hook body to execute.
+ /// A representing the asynchronous operation.
ValueTask ExecuteBeforeClassHook(MethodMetadata hookMethodInfo, ClassHookContext context, Func action);
+
+ ///
+ /// Executes a "before test" hook.
+ ///
+ /// Metadata about the hook method being executed.
+ /// The test context for the test about to execute.
+ /// The hook body to execute.
+ /// A representing the asynchronous operation.
ValueTask ExecuteBeforeTestHook(MethodMetadata hookMethodInfo, TestContext context, Func action);
+ ///
+ /// Executes an "after test discovery" hook.
+ ///
+ /// Metadata about the hook method being executed.
+ /// The test discovery context.
+ /// The hook body to execute.
+ /// A representing the asynchronous operation.
ValueTask ExecuteAfterTestDiscoveryHook(MethodMetadata hookMethodInfo, TestDiscoveryContext context, Func action);
+
+ ///
+ /// Executes an "after test session" hook.
+ ///
+ /// Metadata about the hook method being executed.
+ /// The test session context.
+ /// The hook body to execute.
+ /// A representing the asynchronous operation.
ValueTask ExecuteAfterTestSessionHook(MethodMetadata hookMethodInfo, TestSessionContext context, Func action);
+
+ ///
+ /// Executes an "after assembly" hook.
+ ///
+ /// Metadata about the hook method being executed.
+ /// The assembly hook context.
+ /// The hook body to execute.
+ /// A representing the asynchronous operation.
ValueTask ExecuteAfterAssemblyHook(MethodMetadata hookMethodInfo, AssemblyHookContext context, Func action);
+
+ ///
+ /// Executes an "after class" hook.
+ ///
+ /// Metadata about the hook method being executed.
+ /// The class hook context.
+ /// The hook body to execute.
+ /// A representing the asynchronous operation.
ValueTask ExecuteAfterClassHook(MethodMetadata hookMethodInfo, ClassHookContext context, Func action);
+
+ ///
+ /// Executes an "after test" hook.
+ ///
+ /// Metadata about the hook method being executed.
+ /// The test context for the completed test.
+ /// The hook body to execute.
+ /// A representing the asynchronous operation.
ValueTask ExecuteAfterTestHook(MethodMetadata hookMethodInfo, TestContext context, Func action);
}
diff --git a/TUnit.Core/Interfaces/IHookRegisteredEventReceiver.cs b/TUnit.Core/Interfaces/IHookRegisteredEventReceiver.cs
index 8f3ac3da53..d378740e38 100644
--- a/TUnit.Core/Interfaces/IHookRegisteredEventReceiver.cs
+++ b/TUnit.Core/Interfaces/IHookRegisteredEventReceiver.cs
@@ -1,12 +1,34 @@
namespace TUnit.Core.Interfaces;
///
-/// Interface for hook registered event receivers
+/// Defines an event receiver that is notified when a lifecycle hook is registered with the test framework.
///
+///
+///
+/// Implement this interface to perform custom logic when hooks (such as [Before(Test)],
+/// [After(Class)], etc.) are registered. This is useful for modifying hook behavior,
+/// such as applying timeouts to hooks or logging hook registration.
+///
+///
+/// Built-in attributes such as implement this interface to
+/// apply their configuration to hooks in addition to tests.
+///
+///
+/// The parameter provides access to the hook's metadata
+/// and allows modification of hook properties such as timeout values.
+///
+///
+/// The property can be used to control the execution order
+/// when multiple implementations of this interface exist.
+///
+///
public interface IHookRegisteredEventReceiver : IEventReceiver
{
///
- /// Called when a hook is registered
+ /// Called when a lifecycle hook is registered with the test framework.
///
+ /// The hook registered context containing information about the hook,
+ /// including its method metadata and configurable properties such as timeout.
+ /// A representing the asynchronous operation.
ValueTask OnHookRegistered(HookRegisteredContext context);
}
\ No newline at end of file
diff --git a/TUnit.Core/Interfaces/ITUnitPlugin.cs b/TUnit.Core/Interfaces/ITUnitPlugin.cs
new file mode 100644
index 0000000000..d9d6c32e9b
--- /dev/null
+++ b/TUnit.Core/Interfaces/ITUnitPlugin.cs
@@ -0,0 +1,87 @@
+namespace TUnit.Core.Interfaces;
+
+///
+/// Defines a unified plugin interface for extending TUnit's test lifecycle.
+///
+///
+///
+/// Implement this interface to create a self-contained plugin that can hook into multiple
+/// points of the TUnit test lifecycle. On .NET 8.0+, this interface provides default (no-op)
+/// implementations for all lifecycle methods, so plugins only need to override the methods
+/// they care about.
+///
+///
+/// Plugins are activated by applying them as attributes to test methods, classes, or assemblies
+/// (when the plugin inherits from ), or by inheriting from a test
+/// base class that implements this interface.
+///
+///
+/// The lifecycle methods are called in the following order:
+///
+/// - when a test is registered with the framework
+/// - when a test is discovered (allows configuration changes)
+/// - immediately before a test executes
+/// Test method runs
+/// - immediately after a test completes (pass or fail)
+/// - if a test was skipped instead of executed
+/// - before a failed test is retried
+///
+///
+///
+/// For session, assembly, and class-level events, see ,
+/// , ,
+/// , ,
+/// and .
+///
+///
+///
+///
+/// // A plugin attribute that logs test lifecycle events
+/// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly)]
+/// public class TestLoggingPluginAttribute : TUnitAttribute, ITUnitPlugin
+/// {
+/// public ValueTask OnTestStart(TestContext context)
+/// {
+/// Console.WriteLine($"Starting: {context.TestDetails.TestName}");
+/// return ValueTask.CompletedTask;
+/// }
+///
+/// public ValueTask OnTestEnd(TestContext context)
+/// {
+/// Console.WriteLine($"Finished: {context.TestDetails.TestName} - {context.Result?.Status}");
+/// return ValueTask.CompletedTask;
+/// }
+///
+/// // On .NET 8.0+, other methods have default no-op implementations.
+/// // On older frameworks, all methods must be explicitly implemented.
+/// }
+///
+///
+public interface ITUnitPlugin :
+ ITestRegisteredEventReceiver,
+ ITestDiscoveryEventReceiver,
+ ITestStartEventReceiver,
+ ITestEndEventReceiver,
+ ITestSkippedEventReceiver,
+ ITestRetryEventReceiver
+{
+#if NET
+ ///
+ ValueTask ITestRegisteredEventReceiver.OnTestRegistered(TestRegisteredContext context) => default;
+
+ ///
+ ValueTask ITestDiscoveryEventReceiver.OnTestDiscovered(DiscoveredTestContext context) => default;
+
+ ///
+ ValueTask ITestStartEventReceiver.OnTestStart(TestContext context) => default;
+
+ ///
+ ValueTask ITestEndEventReceiver.OnTestEnd(TestContext context) => default;
+
+ ///
+ ValueTask ITestSkippedEventReceiver.OnTestSkipped(TestContext context) => default;
+
+ ///
+ ValueTask ITestRetryEventReceiver.OnTestRetry(TestContext context, int retryAttempt) => default;
+#endif
+}
diff --git a/TUnit.Core/Interfaces/ITestDiscoveryEventReceiver.cs b/TUnit.Core/Interfaces/ITestDiscoveryEventReceiver.cs
index 028c79bef6..a06935e1cc 100644
--- a/TUnit.Core/Interfaces/ITestDiscoveryEventReceiver.cs
+++ b/TUnit.Core/Interfaces/ITestDiscoveryEventReceiver.cs
@@ -1,12 +1,52 @@
namespace TUnit.Core.Interfaces;
///
-/// Simplified interface for test discovery event receivers
+/// Defines an event receiver that is notified when a test is discovered during the discovery phase.
///
+///
+///
+/// Implement this interface to perform custom logic when a test is discovered, such as
+/// modifying test metadata, setting timeouts, configuring retry logic, or conditionally
+/// skipping tests based on runtime conditions.
+///
+///
+/// This is one of the most commonly implemented event receivers for third-party extensions,
+/// as it allows modifying test behavior before execution begins. Many built-in attributes
+/// such as and implement this interface.
+///
+///
+/// The parameter provides methods to modify the test's
+/// configuration, such as setting retry limits, timeouts, and custom properties.
+///
+///
+/// The property can be used to control the execution order
+/// when multiple implementations of this interface exist.
+///
+///
+///
+///
+/// public class ConditionalSkipReceiver : ITestDiscoveryEventReceiver
+/// {
+/// public int Order => 0;
+///
+/// public ValueTask OnTestDiscovered(DiscoveredTestContext context)
+/// {
+/// if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+/// {
+/// context.SkipTest("This test only runs on Windows");
+/// }
+/// return ValueTask.CompletedTask;
+/// }
+/// }
+///
+///
public interface ITestDiscoveryEventReceiver : IEventReceiver
{
///
- /// Called when a test is discovered
+ /// Called when a test is discovered during the test discovery phase.
///
+ /// The discovered test context, which provides methods to modify the test's
+ /// configuration such as retry limits, timeouts, and skip conditions.
+ /// A representing the asynchronous operation.
ValueTask OnTestDiscovered(DiscoveredTestContext context);
}
diff --git a/TUnit.Core/Interfaces/ITestEndEventReceiver.cs b/TUnit.Core/Interfaces/ITestEndEventReceiver.cs
index 9693c2804b..c7ad9c7030 100644
--- a/TUnit.Core/Interfaces/ITestEndEventReceiver.cs
+++ b/TUnit.Core/Interfaces/ITestEndEventReceiver.cs
@@ -3,13 +3,35 @@ namespace TUnit.Core.Interfaces;
using TUnit.Core.Enums;
///
-/// Simplified interface for test end event receivers
+/// Defines an event receiver that is notified when a test has completed execution.
///
+///
+///
+/// Implement this interface to perform custom logic after a test completes, such as
+/// recording test results, cleaning up per-test resources, publishing metrics,
+/// or capturing diagnostic information on failure.
+///
+///
+/// This event fires regardless of whether the test passed, failed, or threw an exception.
+/// The test result information is available through the parameter.
+///
+///
+/// On .NET 8.0+, the property controls whether the receiver runs
+/// before or after instance-level [After(Test)] hooks. The default is
+/// for backward compatibility.
+///
+///
+/// The property can be used to control the execution order
+/// when multiple implementations of this interface exist.
+///
+///
public interface ITestEndEventReceiver : IEventReceiver
{
///
- /// Called when a test ends
+ /// Called when a test has completed execution.
///
+ /// The test context containing information about the completed test, including its result.
+ /// A representing the asynchronous operation.
ValueTask OnTestEnd(TestContext context);
///
diff --git a/TUnit.Core/Interfaces/ITestFinder.cs b/TUnit.Core/Interfaces/ITestFinder.cs
index 5d29caa3f8..75a8b56e42 100644
--- a/TUnit.Core/Interfaces/ITestFinder.cs
+++ b/TUnit.Core/Interfaces/ITestFinder.cs
@@ -1,9 +1,37 @@
namespace TUnit.Core.Interfaces;
+///
+/// Defines a service for finding and retrieving test contexts by type or method signature.
+///
+///
+///
+/// This interface is used internally by the TUnit framework to locate tests for
+/// dependency resolution (via [DependsOn]) and other scenarios where tests
+/// need to be looked up at runtime.
+///
+///
+/// Third-party extensions can use this interface to query the set of registered tests
+/// for a given class or to find specific tests by their method signature.
+///
+///
public interface ITestFinder
{
+ ///
+ /// Gets all test contexts for tests defined in the specified class type.
+ ///
+ /// The type of the test class to search.
+ /// An enumerable of test contexts for all tests in the specified class.
IEnumerable GetTests(Type classType);
+ ///
+ /// Gets test contexts matching a specific method name and parameter signature.
+ ///
+ /// The name of the test method.
+ /// The types of the test method parameters.
+ /// The type of the test class containing the method.
+ /// The types of the class constructor parameters.
+ /// The class constructor argument values used to match a specific class instance.
+ /// An array of matching test contexts.
TestContext[] GetTestsByNameAndParameters(string testName, IEnumerable methodParameterTypes,
Type classType, IEnumerable classParameterTypes, IEnumerable