diff --git a/TUnit.Engine/Services/TestExecution/TestCoordinator.cs b/TUnit.Engine/Services/TestExecution/TestCoordinator.cs index 29e444588b..cc79d87fb7 100644 --- a/TUnit.Engine/Services/TestExecution/TestCoordinator.cs +++ b/TUnit.Engine/Services/TestExecution/TestCoordinator.cs @@ -61,6 +61,10 @@ private async ValueTask ExecuteTestInternalAsync(AbstractExecutableTest test, Ca _contextRestorer.RestoreContext(test); + // Register event receivers early so that skip event receivers work + // even when the test is skipped before full initialization. + _eventReceiverOrchestrator.RegisterReceivers(test.Context, cancellationToken); + // Check if test was already marked as skipped during registration // (e.g., by a derived SkipAttribute evaluated in OnTestRegistered). // This must be checked before any instance creation or retry/timeout logic. diff --git a/TUnit.TestProject/LastTestEventReceiverTests.cs b/TUnit.TestProject/LastTestEventReceiverTests.cs index 2bd365ff8b..eeb62608f3 100644 --- a/TUnit.TestProject/LastTestEventReceiverTests.cs +++ b/TUnit.TestProject/LastTestEventReceiverTests.cs @@ -109,19 +109,14 @@ public ValueTask OnLastTestInAssembly(AssemblyHookContext context, TestContext t } } -// Test for skipped event receivers +// Test for skipped event receivers using [Skip] attribute. +// After(Test) hooks don't run for statically skipped tests, so we use a +// DependsOn verification test that runs after the skipped test completes. public class SkippedEventReceiverTests { public static readonly List Events = []; public static string? CapturedSkipReason = null; - [Before(Test)] - public void ClearEvents() - { - Events.Clear(); - CapturedSkipReason = null; - } - [Test, Skip("Testing skip event with custom reason")] [SkipEventReceiverAttribute] public async Task SkippedTestWithCustomReason() @@ -129,18 +124,22 @@ public async Task SkippedTestWithCustomReason() await Task.Delay(10); } - [After(Test)] - public async Task VerifySkipEventFired(TestContext context) + [Test] + [DependsOn(nameof(SkippedTestWithCustomReason), ProceedOnFailure = true)] + public async Task Verify_SkipEventReceiver_Fired_Exactly_Once() { - // Give some time for async event receivers to complete - await Task.Delay(100); + // Events were populated by the SkipEventReceiverAttribute on the skipped test. + // No Before(Test) clearing here — we need to observe the cumulative state. + await Assert.That(Events).Contains("TestSkipped"); + await Assert.That(Events).Contains("TestEnd"); + await Assert.That(CapturedSkipReason).IsEqualTo("Testing skip event with custom reason"); - if (context. Metadata.DisplayName.Contains("SkippedTestWithCustomReason")) - { - await Assert.That(Events).Contains("TestSkipped"); - await Assert.That(Events).Contains("TestEnd"); - await Assert.That(CapturedSkipReason).IsEqualTo("Testing skip event with custom reason"); - } + // Guard against double invocation + var skipCount = Events.Count(e => e == "TestSkipped"); + await Assert.That(skipCount).IsEqualTo(1); + + var endCount = Events.Count(e => e == "TestEnd"); + await Assert.That(endCount).IsEqualTo(1); } } @@ -200,7 +199,10 @@ public async Task VerifyRuntimeSkipEventFired(TestContext context) await Assert.That(Events).Contains("TestSkipped"); await Assert.That(Events).Contains("TestEnd"); - // Verify TestEnd is called exactly once (not twice) + // Verify events are called exactly once (not twice) + var testSkippedCount = Events.Count(e => e == "TestSkipped"); + await Assert.That(testSkippedCount).IsEqualTo(1); + var testEndCount = Events.Count(e => e == "TestEnd"); await Assert.That(testEndCount).IsEqualTo(1); }