Skip to content

Commit 1f2b192

Browse files
committed
feat(execution): Refactor OverrideResult method to include state validation and preserve original exceptions
1 parent fe187b9 commit 1f2b192

7 files changed

+103
-41
lines changed

TUnit.Core/Interfaces/ITestExecution.cs

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -48,52 +48,37 @@ public interface ITestExecution
4848
Func<TestContext, Exception, int, Task<bool>>? RetryFunc { get; }
4949

5050
/// <summary>
51-
/// Overrides the test result with a passed state and custom reason.
52-
/// Useful for marking tests as passed under special conditions.
51+
/// Overrides the test result with a specific state and custom reason.
5352
/// </summary>
53+
/// <param name="state">The desired test state (Passed, Failed, Skipped, Timeout, or Cancelled)</param>
5454
/// <param name="reason">The reason for overriding the result (cannot be empty)</param>
55-
/// <exception cref="ArgumentException">Thrown when reason is empty or whitespace</exception>
55+
/// <exception cref="ArgumentException">Thrown when reason is empty, whitespace, or state is invalid (NotStarted, WaitingForDependencies, Queued, Running)</exception>
5656
/// <exception cref="InvalidOperationException">Thrown when result has already been overridden</exception>
5757
/// <remarks>
5858
/// This method can only be called once per test. Subsequent calls will throw an exception.
59+
/// Only final states are allowed: Passed, Failed, Skipped, Timeout, or Cancelled. Intermediate states like Running, Queued, NotStarted, or WaitingForDependencies are rejected.
5960
/// The original exception (if any) is preserved in <see cref="TestResult.OriginalException"/>.
61+
/// When overriding to Failed, the original exception is retained in <see cref="TestResult.Exception"/>.
62+
/// When overriding to Passed or Skipped, the Exception property is cleared but preserved in OriginalException.
6063
/// Best practice: Call this from <see cref="ITestEndEventReceiver.OnTestEnd"/> or After(Test) hooks.
6164
/// </remarks>
6265
/// <example>
6366
/// <code>
67+
/// // Override failed test to passed
6468
/// public class RetryOnInfrastructureErrorAttribute : Attribute, ITestEndEventReceiver
6569
/// {
6670
/// public ValueTask OnTestEnd(TestContext context)
6771
/// {
6872
/// if (context.Result?.Exception is HttpRequestException)
6973
/// {
70-
/// context.Execution.OverrideResult("Infrastructure error - not a test failure");
74+
/// context.Execution.OverrideResult(TestState.Passed, "Infrastructure error - not a test failure");
7175
/// }
7276
/// return default;
7377
/// }
7478
/// public int Order => 0;
7579
/// }
76-
/// </code>
77-
/// </example>
78-
void OverrideResult(string reason);
79-
80-
/// <summary>
81-
/// Overrides the test result with a specific state and custom reason.
82-
/// </summary>
83-
/// <param name="state">The desired test state (Passed, Failed, Skipped, Timeout, or Cancelled)</param>
84-
/// <param name="reason">The reason for overriding the result (cannot be empty)</param>
85-
/// <exception cref="ArgumentException">Thrown when reason is empty, whitespace, or state is invalid (NotStarted, WaitingForDependencies, Queued, Running)</exception>
86-
/// <exception cref="InvalidOperationException">Thrown when result has already been overridden</exception>
87-
/// <remarks>
88-
/// This method can only be called once per test. Subsequent calls will throw an exception.
89-
/// Only final states are allowed: Passed, Failed, Skipped, Timeout, or Cancelled. Intermediate states like Running, Queued, NotStarted, or WaitingForDependencies are rejected.
90-
/// The original exception (if any) is preserved in <see cref="TestResult.OriginalException"/>.
91-
/// When overriding to Failed, the original exception is retained in <see cref="TestResult.Exception"/>.
92-
/// When overriding to Passed or Skipped, the Exception property is cleared but preserved in OriginalException.
93-
/// Best practice: Call this from <see cref="ITestEndEventReceiver.OnTestEnd"/> or After(Test) hooks.
94-
/// </remarks>
95-
/// <example>
96-
/// <code>
80+
///
81+
/// // Override failed test to skipped
9782
/// public class IgnoreOnWeekendAttribute : Attribute, ITestEndEventReceiver
9883
/// {
9984
/// public ValueTask OnTestEnd(TestContext context)

TUnit.Core/TestContext.Execution.cs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,10 @@ bool ITestExecution.ReportResult
6262
set => ReportResult = value;
6363
}
6464

65-
void ITestExecution.OverrideResult(string reason) => OverrideResult(reason);
6665
void ITestExecution.OverrideResult(TestState state, string reason) => OverrideResult(state, reason);
6766
void ITestExecution.AddLinkedCancellationToken(CancellationToken cancellationToken) => AddLinkedCancellationToken(cancellationToken);
6867

6968
// Internal implementation methods
70-
internal void OverrideResult(string reason)
71-
{
72-
OverrideResult(TestState.Passed, reason);
73-
}
74-
7569
internal void OverrideResult(TestState state, string reason)
7670
{
7771
// Validation: Reason must not be empty
@@ -100,6 +94,17 @@ internal void OverrideResult(TestState state, string reason)
10094
// Preserve the original exception if one exists
10195
var originalException = Result?.Exception;
10296

97+
// When overriding to Failed without an original exception, create a synthetic one
98+
Exception? exceptionForResult;
99+
if (state == TestState.Failed)
100+
{
101+
exceptionForResult = originalException ?? new InvalidOperationException($"Test overridden to failed: {reason}");
102+
}
103+
else
104+
{
105+
exceptionForResult = null;
106+
}
107+
103108
Result = new TestResult
104109
{
105110
State = state,
@@ -109,7 +114,7 @@ internal void OverrideResult(TestState state, string reason)
109114
Start = TestStart ?? DateTimeOffset.UtcNow,
110115
End = DateTimeOffset.UtcNow,
111116
Duration = DateTimeOffset.UtcNow - (TestStart ?? DateTimeOffset.UtcNow),
112-
Exception = state == TestState.Failed ? originalException : null,
117+
Exception = exceptionForResult,
113118
ComputerName = Environment.MachineName,
114119
TestContext = this
115120
};

TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2330,7 +2330,6 @@ namespace .Interfaces
23302330
? TestEnd { get; }
23312331
? TestStart { get; }
23322332
void AddLinkedCancellationToken(.CancellationToken cancellationToken);
2333-
void OverrideResult(string reason);
23342333
void OverrideResult(.TestState state, string reason);
23352334
}
23362335
public interface ITestExecutor

TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2330,7 +2330,6 @@ namespace .Interfaces
23302330
? TestEnd { get; }
23312331
? TestStart { get; }
23322332
void AddLinkedCancellationToken(.CancellationToken cancellationToken);
2333-
void OverrideResult(string reason);
23342333
void OverrideResult(.TestState state, string reason);
23352334
}
23362335
public interface ITestExecutor

TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2330,7 +2330,6 @@ namespace .Interfaces
23302330
? TestEnd { get; }
23312331
? TestStart { get; }
23322332
void AddLinkedCancellationToken(.CancellationToken cancellationToken);
2333-
void OverrideResult(string reason);
23342333
void OverrideResult(.TestState state, string reason);
23352334
}
23362335
public interface ITestExecutor

TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2261,7 +2261,6 @@ namespace .Interfaces
22612261
? TestEnd { get; }
22622262
? TestStart { get; }
22632263
void AddLinkedCancellationToken(.CancellationToken cancellationToken);
2264-
void OverrideResult(string reason);
22652264
void OverrideResult(.TestState state, string reason);
22662265
}
22672266
public interface ITestExecutor

TUnit.TestProject/OverrideResultsTests.cs

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,99 @@ namespace TUnit.TestProject;
77
public class OverrideResultsTests
88
{
99
[Test, OverridePass]
10-
public void OverrideResult_Throws_When_TestResult_Is_Null()
10+
public void OverrideFailedTestToPassed()
1111
{
12-
throw new InvalidOperationException();
12+
throw new InvalidOperationException("This test should fail but will be overridden to passed");
13+
}
14+
15+
[Test, OverrideToSkipped]
16+
public void OverrideFailedTestToSkipped()
17+
{
18+
throw new ArgumentException("This test should fail but will be overridden to skipped");
19+
}
20+
21+
[Test, OverrideToFailed]
22+
public void OverridePassedTestToFailed()
23+
{
24+
// This test passes but will be overridden to failed
25+
}
26+
27+
[Test, PreserveOriginalException]
28+
public void VerifyOriginalExceptionIsPreserved()
29+
{
30+
throw new InvalidOperationException("Original exception that should be preserved");
1331
}
1432

1533
[After(Class)]
1634
public static async Task AfterClass(ClassHookContext classHookContext)
1735
{
18-
await Assert.That(classHookContext.Tests).HasSingleItem();
19-
await Assert.That(classHookContext.Tests).ContainsOnly(t => t.Execution.Result?.State == TestState.Passed);
36+
await Assert.That(classHookContext.Tests.Count).IsEqualTo(4);
37+
38+
// Test 1: Failed -> Passed
39+
var test1 = classHookContext.Tests.First(t => t.Metadata.TestDetails.TestName == "OverrideFailedTestToPassed");
40+
await Assert.That(test1.Execution.Result?.State).IsEqualTo(TestState.Passed);
41+
await Assert.That(test1.Execution.Result?.IsOverridden).IsTrue();
42+
await Assert.That(test1.Execution.Result?.OverrideReason).IsEqualTo("Overridden to passed");
43+
await Assert.That(test1.Execution.Result?.OriginalException).IsNotNull();
44+
await Assert.That(test1.Execution.Result?.OriginalException).IsTypeOf<InvalidOperationException>();
45+
46+
// Test 2: Failed -> Skipped
47+
var test2 = classHookContext.Tests.First(t => t.Metadata.TestDetails.TestName == "OverrideFailedTestToSkipped");
48+
await Assert.That(test2.Execution.Result?.State).IsEqualTo(TestState.Skipped);
49+
await Assert.That(test2.Execution.Result?.IsOverridden).IsTrue();
50+
await Assert.That(test2.Execution.Result?.OverrideReason).IsEqualTo("Overridden to skipped");
51+
await Assert.That(test2.Execution.Result?.OriginalException).IsNotNull();
52+
await Assert.That(test2.Execution.Result?.OriginalException).IsTypeOf<ArgumentException>();
53+
54+
// Test 3: Passed -> Failed
55+
var test3 = classHookContext.Tests.First(t => t.Metadata.TestDetails.TestName == "OverridePassedTestToFailed");
56+
await Assert.That(test3.Execution.Result?.State).IsEqualTo(TestState.Failed);
57+
await Assert.That(test3.Execution.Result?.IsOverridden).IsTrue();
58+
await Assert.That(test3.Execution.Result?.OverrideReason).IsEqualTo("Overridden to failed");
59+
60+
// Test 4: Verify original exception preservation
61+
var test4 = classHookContext.Tests.First(t => t.Metadata.TestDetails.TestName == "VerifyOriginalExceptionIsPreserved");
62+
await Assert.That(test4.Execution.Result?.OriginalException?.Message).IsEqualTo("Original exception that should be preserved");
2063
}
2164

2265
public class OverridePassAttribute : Attribute, ITestEndEventReceiver
2366
{
2467
public ValueTask OnTestEnd(TestContext afterTestContext)
2568
{
26-
afterTestContext.Execution.OverrideResult(TestState.Passed, "Because I said so");
69+
afterTestContext.Execution.OverrideResult(TestState.Passed, "Overridden to passed");
70+
return default(ValueTask);
71+
}
72+
73+
public int Order => 0;
74+
}
75+
76+
public class OverrideToSkippedAttribute : Attribute, ITestEndEventReceiver
77+
{
78+
public ValueTask OnTestEnd(TestContext afterTestContext)
79+
{
80+
afterTestContext.Execution.OverrideResult(TestState.Skipped, "Overridden to skipped");
81+
return default(ValueTask);
82+
}
83+
84+
public int Order => 0;
85+
}
86+
87+
public class OverrideToFailedAttribute : Attribute, ITestEndEventReceiver
88+
{
89+
public ValueTask OnTestEnd(TestContext afterTestContext)
90+
{
91+
afterTestContext.Execution.OverrideResult(TestState.Failed, "Overridden to failed");
92+
return default(ValueTask);
93+
}
94+
95+
public int Order => 0;
96+
}
97+
98+
public class PreserveOriginalExceptionAttribute : Attribute, ITestEndEventReceiver
99+
{
100+
public ValueTask OnTestEnd(TestContext afterTestContext)
101+
{
102+
afterTestContext.Execution.OverrideResult(TestState.Passed, "Test passed after retry");
27103
return default(ValueTask);
28104
}
29105

0 commit comments

Comments
 (0)