From 768c61fb67a7e130e735b93beaf782840571aa40 Mon Sep 17 00:00:00 2001 From: Mike Curn Date: Fri, 27 Mar 2026 12:48:49 -0700 Subject: [PATCH 1/2] Pass the test immediately if all test blocks pass and expose TestBlockException get and FinallyBlockExceptions --- .../TestCase.cs | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/IntelliTect.TestTools.TestFramework/TestCase.cs b/IntelliTect.TestTools.TestFramework/TestCase.cs index 4efdffc..dcd29a4 100644 --- a/IntelliTect.TestTools.TestFramework/TestCase.cs +++ b/IntelliTect.TestTools.TestFramework/TestCase.cs @@ -37,22 +37,35 @@ public TestCase(string testCaseName, string testMethodName, int testCaseId, ISer /// If a finally block fails and this property is true, the test case is still considered passed internally, but most unit test frameworks will mark the test failed. /// public bool ThrowOnFinallyBlockException { get; set; } = true; + /// + /// If any test block throws an exception, it will be stored here. Finally block exceptions are stored separately in FinallyBlockExceptions. If this is not null at the end of the test case execution, the test case is considered failed. + /// + public Exception? TestBlockException { get; private set; } + /// + /// Gets the list of exceptions that were thrown during the execution of finally blocks. + /// + /// This collection contains all exceptions that occurred in finally blocks, allowing + /// callers to inspect or handle them after execution. The list is empty if no exceptions were thrown in any + /// finally block. + public List FinallyBlockExceptions { get; } = []; + /// + /// Did the test case pass? This is determined by whether any test block threw an exception. + /// + /// + /// Finally block exceptions do not cause the test case to be marked as failed, but they are still captured and can cause the test case to throw after execution if ThrowOnFinallyBlockException is true. + /// + public bool Passed { get; set; } // May make sense to make some of the below public if it's needed for debugging. // If so, definitely need to change them to internal or private sets. - internal List TestBlocks { get; set; } = new(); - internal List FinallyBlocks { get; set; } = new(); + internal List TestBlocks { get; set; } = []; + internal List FinallyBlocks { get; set; } = []; internal bool HasLogger { get; set; } = true; private ITestCaseLogger? Log { get; set; } private IServiceCollection ServiceCollection { get; } - private Dictionary BlockOutput { get; } = new(); - private Exception? TestBlockException { get; set; } - private List FinallyBlockExceptions { get; } = new(); - - // Has this test case passed? Will only be true if every regular test block succeeds. - public bool Passed { get; set; } + private Dictionary BlockOutput { get; } = []; /// /// Legacy method signature. Executes the test case. NOTE: Prefer to use ExecuteAsync, even if you have no awaitable test blocks. From 9bfdefc89778c5010b514414fbfd8f9e5f6a091a Mon Sep 17 00:00:00 2001 From: Mike Curn Date: Fri, 27 Mar 2026 14:25:50 -0700 Subject: [PATCH 2/2] Actually pass the test case before running finally blocks (oops) --- .../TestCaseTests/FinallyExecutionTests.cs | 39 +++++++++++++++++++ .../TestData/Dependencies/SimulatorClasses.cs | 8 ++++ .../TestCase.cs | 24 +++++------- 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/IntelliTect.TestTools.TestFramework.Tests/TestCaseTests/FinallyExecutionTests.cs b/IntelliTect.TestTools.TestFramework.Tests/TestCaseTests/FinallyExecutionTests.cs index 45196d4..78277eb 100644 --- a/IntelliTect.TestTools.TestFramework.Tests/TestCaseTests/FinallyExecutionTests.cs +++ b/IntelliTect.TestTools.TestFramework.Tests/TestCaseTests/FinallyExecutionTests.cs @@ -3,6 +3,7 @@ using System; using System.Threading.Tasks; using Xunit; +using Xunit.Sdk; namespace IntelliTect.TestTools.TestFramework.Tests.TestCaseTests { @@ -201,5 +202,43 @@ public async Task OnlyTestBlockThrowsExpectedExceptionWhenOverridingDefaultFinal // Assert Assert.False(tc.Passed, "Test case did not get marked as Failed when we expected it."); } + + [Fact] + public async Task TestCasePassedIsSetTrueBeforeFinallyBlocksRun() + { + // Arrange + TestCase tc = new TestBuilder() + .AddDependencyInstance(true) + .AddTestBlock() + .AddFinallyBlock() + .Build(); + + // Act + await tc.ExecuteAsync(); + + // Assert + Assert.True(tc.Passed, "Test case did not get marked as Passed when we expected it."); + } + + [Fact] + public async Task TestCasePassedIsSetTrueEvenIfFinallyBlockFails() + { + // Arrange + TestCase tc = new TestBuilder() + .AddDependencyInstance(false) + .AddTestBlock() + .AddFinallyBlock() + .Build(); + + // Act + AggregateException exception = await Assert.ThrowsAsync(() => tc.ExecuteAsync()); + + // Assert + Assert.Equal(2, exception.InnerExceptions.Count); + Assert.Equal(typeof(DivideByZeroException), exception.InnerException?.GetType()); + Assert.Equal(typeof(DivideByZeroException), exception.InnerExceptions[0].GetType()); + Assert.Equal(typeof(TrueException), exception.InnerExceptions[1].GetType()); + Assert.False(tc.Passed, "Test case was marked as Passed when we did not expected it."); + } } } diff --git a/IntelliTect.TestTools.TestFramework.Tests/TestData/Dependencies/SimulatorClasses.cs b/IntelliTect.TestTools.TestFramework.Tests/TestData/Dependencies/SimulatorClasses.cs index a5ed5d9..fe12e9f 100644 --- a/IntelliTect.TestTools.TestFramework.Tests/TestData/Dependencies/SimulatorClasses.cs +++ b/IntelliTect.TestTools.TestFramework.Tests/TestData/Dependencies/SimulatorClasses.cs @@ -134,4 +134,12 @@ public async Task Execute() await Task.Delay(1); } } + + public class ExampleBlockCheckingTestSuccess : TestBlock + { + public void Execute(TestCase tc) + { + Assert.True(tc.Passed); + } + } } diff --git a/IntelliTect.TestTools.TestFramework/TestCase.cs b/IntelliTect.TestTools.TestFramework/TestCase.cs index dcd29a4..04a6731 100644 --- a/IntelliTect.TestTools.TestFramework/TestCase.cs +++ b/IntelliTect.TestTools.TestFramework/TestCase.cs @@ -114,6 +114,16 @@ public async Task ExecuteAsync() } } + if (TestBlockException is null) + { + Passed = true; + Log?.Info("Test case finished successfully."); + } + else + { + Log?.Critical($"Test case failed and will continue to executing Finally blocks. Exception: {TestBlockException}"); + } + foreach (var fb in FinallyBlocks) { if (Log is not null) Log.CurrentTestBlock = fb.Type.ToString(); @@ -129,20 +139,6 @@ public async Task ExecuteAsync() Log?.Critical($"Finally block failed: {FinallyBlockExceptions.LastOrDefault()}"); } } - - if (TestBlockException is null) - { - // Note: This likely needs to be moved up above the finally blocks. - // If a test case "passes" i.e. finishes all of its test blocks, we probably need to know that - // in the finally blocks. - // Need to think about how to handle "passed" state if a finally block fails. - Passed = true; - Log?.Info("Test case finished successfully."); - } - else - { - Log?.Critical($"Test case failed: {TestBlockException}"); - } } services.Dispose();