diff --git a/TUnit.Core/Attributes/TestMetadata/NotDiscoverableAttribute.cs b/TUnit.Core/Attributes/TestMetadata/NotDiscoverableAttribute.cs
new file mode 100644
index 0000000000..0ad78f372a
--- /dev/null
+++ b/TUnit.Core/Attributes/TestMetadata/NotDiscoverableAttribute.cs
@@ -0,0 +1,100 @@
+using TUnit.Core.Interfaces;
+
+namespace TUnit.Core;
+
+///
+/// Specifies that a test method, test class, or assembly should be hidden from test discovery/explorer.
+///
+///
+///
+/// When applied to a test method, class, or assembly, the NotDiscoverableAttribute prevents the test(s)
+/// from appearing in test explorers and IDE test runners, while still allowing them to execute normally
+/// when run via filters or direct invocation.
+///
+///
+/// This is useful for infrastructure tests, internal helpers, or tests that should only be run
+/// as dependencies of other tests.
+///
+///
+///
+///
+/// // Simple usage - hide test from explorer
+/// [Test]
+/// [NotDiscoverable]
+/// public void InfrastructureSetupTest()
+/// {
+/// // This test will not appear in test explorer but can still be executed
+/// }
+///
+/// // With reason for documentation
+/// [Test]
+/// [NotDiscoverable("Internal fixture helper - not meant to be run directly")]
+/// public void SharedFixtureSetup()
+/// {
+/// // Hidden from discovery
+/// }
+///
+/// // Conditional hiding via inheritance
+/// public class NotDiscoverableOnCIAttribute : NotDiscoverableAttribute
+/// {
+/// public NotDiscoverableOnCIAttribute() : base("Hidden on CI") { }
+///
+/// public override Task<bool> ShouldHide(TestRegisteredContext context)
+/// {
+/// return Task.FromResult(Environment.GetEnvironmentVariable("CI") == "true");
+/// }
+/// }
+///
+///
+[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = false, Inherited = true)]
+public class NotDiscoverableAttribute : TUnitAttribute, ITestRegisteredEventReceiver
+{
+ ///
+ /// Gets the reason why this test is hidden from discovery.
+ ///
+ public string? Reason { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public NotDiscoverableAttribute()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class with a reason.
+ ///
+ /// The reason why this test is hidden from discovery.
+ public NotDiscoverableAttribute(string reason)
+ {
+ Reason = reason;
+ }
+
+ ///
+ public int Order => int.MinValue;
+
+ ///
+ public async ValueTask OnTestRegistered(TestRegisteredContext context)
+ {
+ if (await ShouldHide(context))
+ {
+ context.TestContext.IsNotDiscoverable = true;
+ }
+ }
+
+ ///
+ /// Determines whether the test should be hidden from discovery.
+ ///
+ /// The test context containing information about the test being registered.
+ ///
+ /// A task that represents the asynchronous operation.
+ /// The task result is true if the test should be hidden; otherwise, false.
+ ///
+ ///
+ /// Can be overridden in derived classes to implement conditional hiding logic
+ /// based on specific conditions or criteria.
+ ///
+ /// The default implementation always returns true, meaning the test will always be hidden.
+ ///
+ public virtual Task ShouldHide(TestRegisteredContext context) => Task.FromResult(true);
+}
diff --git a/TUnit.Core/Interfaces/ITestExecution.cs b/TUnit.Core/Interfaces/ITestExecution.cs
index 16a3bd6d50..efab81b9c0 100644
--- a/TUnit.Core/Interfaces/ITestExecution.cs
+++ b/TUnit.Core/Interfaces/ITestExecution.cs
@@ -107,6 +107,13 @@ public interface ITestExecution
///
bool ReportResult { get; set; }
+ ///
+ /// Gets or sets whether the test should be hidden from test discovery/explorer.
+ /// Defaults to false. Set to true to hide the test from discovery notifications
+ /// while still allowing it to execute when run directly.
+ ///
+ bool IsNotDiscoverable { get; set; }
+
///
/// Links an external cancellation token to this test's execution token.
/// Useful for coordinating cancellation across multiple operations or tests.
diff --git a/TUnit.Core/TestContext.Execution.cs b/TUnit.Core/TestContext.Execution.cs
index 1ac299776c..2a45e0848d 100644
--- a/TUnit.Core/TestContext.Execution.cs
+++ b/TUnit.Core/TestContext.Execution.cs
@@ -22,6 +22,7 @@ public partial class TestContext
internal Func>? RetryFunc { get; set; }
internal IHookExecutor? CustomHookExecutor { get; set; }
internal bool ReportResult { get; set; } = true;
+ internal bool IsNotDiscoverable { get; set; }
// Explicit interface implementations for ITestExecution
TestPhase ITestExecution.Phase => Phase;
@@ -62,6 +63,11 @@ bool ITestExecution.ReportResult
get => ReportResult;
set => ReportResult = value;
}
+ bool ITestExecution.IsNotDiscoverable
+ {
+ get => IsNotDiscoverable;
+ set => IsNotDiscoverable = value;
+ }
void ITestExecution.OverrideResult(TestState state, string reason) => OverrideResult(state, reason);
void ITestExecution.AddLinkedCancellationToken(CancellationToken cancellationToken) => AddLinkedCancellationToken(cancellationToken);
diff --git a/TUnit.Engine/TUnitMessageBus.cs b/TUnit.Engine/TUnitMessageBus.cs
index 00a6212010..0eecd07bfd 100644
--- a/TUnit.Engine/TUnitMessageBus.cs
+++ b/TUnit.Engine/TUnitMessageBus.cs
@@ -23,6 +23,11 @@ internal class TUnitMessageBus(IExtension extension, ICommandLineOptions command
public async ValueTask Discovered(TestContext testContext)
{
+ if (testContext.IsNotDiscoverable)
+ {
+ return;
+ }
+
await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(
sessionUid: _sessionSessionUid,
testNode: testContext.ToTestNode(DiscoveredTestNodeStateProperty.CachedInstance)
diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt
index 23efc08b5c..693efbbaa0 100644
--- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt
+++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt
@@ -1004,6 +1004,16 @@ namespace
public override int GetHashCode() { }
protected virtual bool PrintMembers(.StringBuilder stringBuilder) { }
}
+ [(.Assembly | .Class | .Method, AllowMultiple=false, Inherited=true)]
+ public class NotDiscoverableAttribute : .TUnitAttribute, ., .
+ {
+ public NotDiscoverableAttribute() { }
+ public NotDiscoverableAttribute(string reason) { }
+ public int Order { get; }
+ public string? Reason { get; }
+ public . OnTestRegistered(.TestRegisteredContext context) { }
+ public virtual . ShouldHide(.TestRegisteredContext context) { }
+ }
[(.Assembly | .Class | .Method)]
public class NotInParallelAttribute : .SingleTUnitAttribute, .IScopedAttribute, ., .
{
@@ -2412,6 +2422,7 @@ namespace .Interfaces
.CancellationToken CancellationToken { get; }
int CurrentRetryAttempt { get; }
.? CustomHookExecutor { get; set; }
+ bool IsNotDiscoverable { get; set; }
.TestPhase Phase { get; }
bool ReportResult { get; set; }
.TestResult? Result { get; }
diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
index 83d0d32987..3a5b603267 100644
--- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
+++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
@@ -1004,6 +1004,16 @@ namespace
public override int GetHashCode() { }
protected virtual bool PrintMembers(.StringBuilder stringBuilder) { }
}
+ [(.Assembly | .Class | .Method, AllowMultiple=false, Inherited=true)]
+ public class NotDiscoverableAttribute : .TUnitAttribute, ., .
+ {
+ public NotDiscoverableAttribute() { }
+ public NotDiscoverableAttribute(string reason) { }
+ public int Order { get; }
+ public string? Reason { get; }
+ public . OnTestRegistered(.TestRegisteredContext context) { }
+ public virtual . ShouldHide(.TestRegisteredContext context) { }
+ }
[(.Assembly | .Class | .Method)]
public class NotInParallelAttribute : .SingleTUnitAttribute, .IScopedAttribute, ., .
{
@@ -2412,6 +2422,7 @@ namespace .Interfaces
.CancellationToken CancellationToken { get; }
int CurrentRetryAttempt { get; }
.? CustomHookExecutor { get; set; }
+ bool IsNotDiscoverable { get; set; }
.TestPhase Phase { get; }
bool ReportResult { get; set; }
.TestResult? Result { get; }
diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
index 36ed6038ef..529f3101a9 100644
--- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
+++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
@@ -1004,6 +1004,16 @@ namespace
public override int GetHashCode() { }
protected virtual bool PrintMembers(.StringBuilder stringBuilder) { }
}
+ [(.Assembly | .Class | .Method, AllowMultiple=false, Inherited=true)]
+ public class NotDiscoverableAttribute : .TUnitAttribute, ., .
+ {
+ public NotDiscoverableAttribute() { }
+ public NotDiscoverableAttribute(string reason) { }
+ public int Order { get; }
+ public string? Reason { get; }
+ public . OnTestRegistered(.TestRegisteredContext context) { }
+ public virtual . ShouldHide(.TestRegisteredContext context) { }
+ }
[(.Assembly | .Class | .Method)]
public class NotInParallelAttribute : .SingleTUnitAttribute, .IScopedAttribute, ., .
{
@@ -2412,6 +2422,7 @@ namespace .Interfaces
.CancellationToken CancellationToken { get; }
int CurrentRetryAttempt { get; }
.? CustomHookExecutor { get; set; }
+ bool IsNotDiscoverable { get; set; }
.TestPhase Phase { get; }
bool ReportResult { get; set; }
.TestResult? Result { get; }
diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt
index 387faefc64..de5560aeb5 100644
--- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt
+++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt
@@ -967,6 +967,16 @@ namespace
public override int GetHashCode() { }
protected virtual bool PrintMembers(.StringBuilder stringBuilder) { }
}
+ [(.Assembly | .Class | .Method, AllowMultiple=false, Inherited=true)]
+ public class NotDiscoverableAttribute : .TUnitAttribute, ., .
+ {
+ public NotDiscoverableAttribute() { }
+ public NotDiscoverableAttribute(string reason) { }
+ public int Order { get; }
+ public string? Reason { get; }
+ public . OnTestRegistered(.TestRegisteredContext context) { }
+ public virtual . ShouldHide(.TestRegisteredContext context) { }
+ }
[(.Assembly | .Class | .Method)]
public class NotInParallelAttribute : .SingleTUnitAttribute, .IScopedAttribute, ., .
{
@@ -2342,6 +2352,7 @@ namespace .Interfaces
.CancellationToken CancellationToken { get; }
int CurrentRetryAttempt { get; }
.? CustomHookExecutor { get; set; }
+ bool IsNotDiscoverable { get; set; }
.TestPhase Phase { get; }
bool ReportResult { get; set; }
.TestResult? Result { get; }
diff --git a/TUnit.TestProject/NotDiscoverableTests.cs b/TUnit.TestProject/NotDiscoverableTests.cs
new file mode 100644
index 0000000000..86dde5bd74
--- /dev/null
+++ b/TUnit.TestProject/NotDiscoverableTests.cs
@@ -0,0 +1,61 @@
+namespace TUnit.TestProject;
+
+public class NotDiscoverableTests
+{
+ [Test]
+ [NotDiscoverable]
+ public void Test_WithNotDiscoverable_ShouldNotAppearInDiscovery()
+ {
+ // This test should execute but not appear in test explorer
+ }
+
+ [Test]
+ [NotDiscoverable("Infrastructure test")]
+ public void Test_WithNotDiscoverableAndReason_ShouldNotAppearInDiscovery()
+ {
+ // This test should execute but not appear in test explorer
+ }
+
+ [Test]
+ public void Test_WithoutNotDiscoverable_ShouldAppearInDiscovery()
+ {
+ // This test should appear normally in test explorer
+ }
+}
+
+[NotDiscoverable]
+public class NotDiscoverableClassTests
+{
+ [Test]
+ public void Test_InNotDiscoverableClass_ShouldNotAppearInDiscovery()
+ {
+ // All tests in this class should be hidden from discovery
+ }
+
+ [Test]
+ public void AnotherTest_InNotDiscoverableClass_ShouldNotAppearInDiscovery()
+ {
+ // All tests in this class should be hidden from discovery
+ }
+}
+
+public class ConditionalNotDiscoverableAttribute : NotDiscoverableAttribute
+{
+ public ConditionalNotDiscoverableAttribute() : base("Conditionally hidden") { }
+
+ public override Task ShouldHide(TestRegisteredContext context)
+ {
+ // Only hide if environment variable is set
+ return Task.FromResult(Environment.GetEnvironmentVariable("HIDE_TEST") == "true");
+ }
+}
+
+public class ConditionalNotDiscoverableTests
+{
+ [Test]
+ [ConditionalNotDiscoverable]
+ public void Test_WithConditionalNotDiscoverable_HidesBasedOnCondition()
+ {
+ // This test is hidden only when HIDE_TEST=true
+ }
+}