Skip to content

feat: forward the caught exception as InnerException when a delegate expectation fails - #1015

Merged
vbreuss merged 3 commits into
mainfrom
feat/forward-caught-exception-as-inner-exception
Sep 2, 2026
Merged

feat: forward the caught exception as InnerException when a delegate expectation fails#1015
vbreuss merged 3 commits into
mainfrom
feat/forward-caught-exception-as-inner-exception

Conversation

@vbreuss

@vbreuss vbreuss commented Sep 1, 2026

Copy link
Copy Markdown
Member

When a delegate expectation failed because the delegate threw, the caught exception was rendered into the failure message as type name plus Message and then discarded, so the original stack trace was unrecoverable and the test framework had no inner exception to display.

ConstraintResult now exposes FailureCause, which the delegate constraints (DoesNotThrow, DoesNotThrowExactly, Throws, ThrowsExactly, ExecutesWithin and the with-value variants) set to the caught exception, and which every wrapping and combining constraint result forwards. It is passed through Fail.Test(string, Exception?) down to the test framework adapter, which attaches it as InnerException of the framework-specific assertion exception.

The failure message text is unchanged.

@vbreuss vbreuss self-assigned this Sep 1, 2026
@vbreuss vbreuss added enhancement New feature or request breaking change The changes require a new major version labels Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Test Results

    7 files   -      6      7 suites   - 6   47s ⏱️ - 3m 24s
1 332 tests  - 16 843  1 331 ✅  - 16 842  1 💤  - 1  0 ❌ ±0 
7 342 runs   - 44 896  7 341 ✅  - 44 895  1 💤  - 1  0 ❌ ±0 

Results for commit 88fe910. ± Comparison against base commit 24dc08d.

This pull request removes 16992 and adds 149 tests. Note that renamed tests count towards both.
aweXpect.Analyzers.Tests.AwaitExpectationAnalyzerTests ‑ WhenAwaited_ShouldNotBeFlagged
aweXpect.Analyzers.Tests.AwaitExpectationAnalyzerTests ‑ WhenAwaited_WithoutReturnValue_ShouldNotBeFlagged
aweXpect.Analyzers.Tests.AwaitExpectationAnalyzerTests ‑ WhenNotAwaited_ShouldBeFlagged
aweXpect.Analyzers.Tests.AwaitExpectationAnalyzerTests ‑ WhenNotAwaited_WithoutReturnValue_ShouldBeFlagged
aweXpect.Analyzers.Tests.AwaitExpectationAnalyzerTests ‑ WhenNotAwaited_WithoutReturnValue_WithVerifyInMethod_ShouldStillBeFlagged
aweXpect.Analyzers.Tests.AwaitExpectationAnalyzerTests ‑ WhenVerifiedStatically_ShouldNotBeFlagged
aweXpect.Analyzers.Tests.AwaitExpectationAnalyzerTests ‑ WhenVerifiedStatically_WithoutReturnValue_ShouldNotBeFlagged
aweXpect.Analyzers.Tests.AwaitExpectationAnalyzerTests ‑ WhenVerifiedWithStaticUsing_ShouldNotBeFlagged
aweXpect.Analyzers.Tests.AwaitExpectationAnalyzerTests ‑ WhenVerifiedWithStaticUsing_WithoutReturnValue_ShouldNotBeFlagged
aweXpect.Analyzers.Tests.AwaitExpectationAnalyzerTests ‑ WhenVerified_ShouldNotBeFlagged
…
aweXpect.Core.Tests.Core.Constraints.ConstraintResultExtensionsTests+AppendExpectationTextTests ‑ FailureCause_ShouldBeForwardedFromInner
aweXpect.Core.Tests.Core.Constraints.ConstraintResultExtensionsTests+FailTests ‑ FailureCause_ShouldBeForwardedFromInner
aweXpect.Core.Tests.Core.Constraints.ConstraintResultExtensionsTests+FailTests ‑ FailureCause_WhenInnerHasNoFailureCause_ShouldBeNull
aweXpect.Core.Tests.Core.Constraints.ConstraintResultExtensionsTests+UseValueTests ‑ FailureCause_ShouldBeForwardedFromInner
aweXpect.Core.Tests.Core.Constraints.ConstraintResultTests+FromExceptionTests ‑ FailureCause_ShouldBeTheException
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ InnerException_ShouldBeSet(message: "message2f95331a-2e4f-4972-a6a0-14f2d20e5026")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ InnerException_ShouldBeSet(message: "message48896fae-95bf-4d84-a9fa-a4474d486496")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ InnerException_ShouldBeSet(message: "message5906bb60-c0f8-4c99-b42a-0d57a879edaf")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ InnerException_ShouldBeSet(message: "messageefed8c01-24b9-4d25-83ab-14097882096a")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ InnerException_ShouldBeSet(message: "messagef17d4f60-597c-4bb9-aad8-22b6f9af0e57")
…

♻️ This comment has been updated with latest results.

@vbreuss
vbreuss force-pushed the feat/forward-caught-exception-as-inner-exception branch from ea9d228 to 4470295 Compare September 2, 2026 18:49
…e expectation fails

When a delegate expectation failed because the delegate threw, the caught exception was rendered into the failure message as type name plus `Message` and then discarded, so the original stack trace was unrecoverable and the test framework had no inner exception to display.

`ConstraintResult` now exposes `FailureCause`, which the delegate constraints (`DoesNotThrow`, `DoesNotThrowExactly`, `Throws`, `ThrowsExactly`, `ExecutesWithin` and the with-value variants) set to the caught exception, and which every wrapping and combining constraint result forwards. It is passed through `Fail.Test(string, Exception?)` down to the test framework adapter, which attaches it as `InnerException` of the framework-specific assertion exception.

The failure message text is unchanged.
@vbreuss
vbreuss force-pushed the feat/forward-caught-exception-as-inner-exception branch from 4470295 to bb3131e Compare September 2, 2026 18:50
…reCause`

- Replace the optional `innerException` parameter of `Fail.Test`/`Fail.Unless`/`Fail.When` with
  dedicated overloads, so that callers compiled against a previous aweXpect.Core version stay
  binary compatible
- Fix the misleading doc comment on the test framework adapter `Fail` method
- Add tests in aweXpect.Core.Tests covering the `FailureCause` forwarding (delegate constraints,
  nodes, constraint result wrappers and the new `Fail`/`FailException` overloads) to satisfy the
  coverage quality gate
@sonarqubecloud

sonarqubecloud Bot commented Sep 2, 2026

Copy link
Copy Markdown

@vbreuss
vbreuss marked this pull request as ready for review September 2, 2026 19:36
Copilot AI lite review requested due to automatic review settings September 2, 2026 19:36
@vbreuss
vbreuss merged commit 2d32dc7 into main Sep 2, 2026
13 checks passed
@vbreuss
vbreuss deleted the feat/forward-caught-exception-as-inner-exception branch September 2, 2026 19:36

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The pipeline build scope flag is set to CoreOnly (which can unintentionally reduce CI/package coverage), and there are framework adapter test names that need correction to avoid misleading diagnostics.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR preserves the original exception (including stack trace) when a delegate-based expectation fails due to a thrown exception, by threading that exception through ConstraintResult.FailureCause into Fail.Test(..., Exception?) and finally into framework-specific assertion exceptions as InnerException (without changing the failure message text).

Changes:

  • Add ConstraintResult.FailureCause and forward it through combining/wrapping results and delegate constraints.
  • Extend failing APIs/adapters (Fail.*, ITestFrameworkAdapter.Fail) to accept an optional inner exception and pass it to the underlying test framework assertion exception.
  • Add/expand tests across core and framework adapter test suites to assert InnerException forwarding behavior.
File summaries
File Description
Tests/Frameworks/aweXpect.Frameworks.Xunit3.Tests/Xunit3TestFrameworkTests.cs Adds Xunit3 adapter test ensuring failure cause becomes InnerException.
Tests/Frameworks/aweXpect.Frameworks.Xunit3.Core.Tests/Xunit3CoreTestFrameworkTests.cs Adds Xunit3-core adapter test for forwarding InnerException.
Tests/Frameworks/aweXpect.Frameworks.Xunit2.Tests/Xunit2TestFrameworkTests.cs Adds Xunit2 adapter test for InnerException forwarding.
Tests/Frameworks/aweXpect.Frameworks.Tunit.Tests/TunitTestFrameworkTests.cs Adds TUnit adapter test for InnerException forwarding.
Tests/Frameworks/aweXpect.Frameworks.NUnit4.Tests/NUnit4TestFrameworkTests.cs Adds NUnit4 adapter test for InnerException forwarding.
Tests/Frameworks/aweXpect.Frameworks.NUnit3.Tests/NUnit3TestFrameworkTests.cs Adds NUnit3 adapter test for InnerException forwarding.
Tests/Frameworks/aweXpect.Frameworks.MsTest4.Tests/MsTest4FrameworkTests.cs Adds MSTest adapter test for InnerException forwarding.
Tests/Frameworks/aweXpect.Frameworks.MsTest3.Tests/MsTest3FrameworkTests.cs Adds MSTest3 adapter test for InnerException forwarding.
Tests/Frameworks/aweXpect.Frameworks.Fallback.Tests/FallbackTests.cs Validates fallback exception includes InnerException when provided.
Tests/aweXpect.Tests/Delegates/ThatDelegate.Throws.Within.Tests.cs Adds coverage for not forwarding when failure is due to timeout (“too late”).
Tests/aweXpect.Tests/Delegates/ThatDelegate.Throws.Tests.cs Adds coverage for forwarding when the “wrong” exception type is thrown.
Tests/aweXpect.Tests/Delegates/ThatDelegate.DoesNotThrowExactly.GenericTests.cs Adds coverage for forwarding when DoesNotThrowExactly fails due to thrown exception.
Tests/aweXpect.Tests/Delegates/ThatDelegate.DoesNotThrow.Tests.cs Adds coverage for forwarding when DoesNotThrow fails due to thrown exception.
Tests/aweXpect.Core.Tests/FailTests.cs Adds coverage for Fail.Test/Unless/When forwarding inner exceptions.
Tests/aweXpect.Core.Tests/Delegates/ThatDelegateTests.cs New core-level tests validating FailureCause propagates through composed delegate expectations.
Tests/aweXpect.Core.Tests/Core/Nodes/WhichNodeTests.cs Adds FailureCause forwarding tests for WhichNode.
Tests/aweXpect.Core.Tests/Core/Nodes/OrNodeTests.cs Adds FailureCause forwarding tests for OrNode.
Tests/aweXpect.Core.Tests/Core/Nodes/AndNodeTests.cs Adds FailureCause forwarding tests for AndNode.
Tests/aweXpect.Core.Tests/Core/Initialization/AweXpectInitializationTests.cs Updates test adapters to include the new Fail(message, innerException) overload.
Tests/aweXpect.Core.Tests/Core/Exceptions/FailExceptionTests.cs Adds coverage for FailException(message, innerException) behavior.
Tests/aweXpect.Core.Tests/Core/Constraints/ConstraintResultTests.FromExceptionTests.cs Adds coverage that FromException exposes FailureCause.
Tests/aweXpect.Core.Tests/Core/Constraints/ConstraintResultExtensionsTests.cs Adds coverage for FailureCause forwarding through result wrappers.
Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_netstandard2.0.txt Updates API baseline for new overloads/property.
Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_net8.0.txt Updates API baseline for new overloads/property.
Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_net10.0.txt Updates API baseline for new overloads/property.
Source/aweXpect/That/Delegates/ThatDelegate.ExecutesWithin.cs Ensures ExecutesWithin constraints expose FailureCause when failing due to exception.
Source/aweXpect.Frameworks/FrameworkGenerator.cs Generates adapter Fail(message, innerException) overloads and (xUnit v3 core) an exception type with inner-exception ctor.
Source/aweXpect.Core/Results/ExpectationResult.cs Passes FailureCause into Fail.Test when throwing on failed expectations.
Source/aweXpect.Core/Results/Expectation.cs Aggregates and forwards FailureCause through combination results and final Fail.Test.
Source/aweXpect.Core/Results/DelegateWithValueResult.cs Exposes FailureCause for delegate-with-value “does not throw” failures.
Source/aweXpect.Core/Fail.cs Adds Fail.Test/Unless/When overloads that accept an optional inner exception.
Source/aweXpect.Core/Delegates/ThatDelegate.WithValue.DoesNotThrowExactly.cs Adds FailureCause forwarding for value delegate constraint.
Source/aweXpect.Core/Delegates/ThatDelegate.WithValue.DoesNotThrow.cs Adds FailureCause forwarding for value delegate constraint.
Source/aweXpect.Core/Delegates/ThatDelegate.WithoutValue.DoesNotThrowExactly.cs Adds FailureCause forwarding for void delegate constraint.
Source/aweXpect.Core/Delegates/ThatDelegate.WithoutValue.DoesNotThrow.cs Adds FailureCause forwarding for void delegate constraint.
Source/aweXpect.Core/Delegates/ThatDelegate.ThrowsExactly.cs Adds FailureCause forwarding when ThrowsExactly fails due to exception.
Source/aweXpect.Core/Delegates/ThatDelegate.Throws.cs Adds FailureCause forwarding for Throws, with special-casing to avoid forwarding on “too late” timeout failures.
Source/aweXpect.Core/Core/Nodes/WhichNode.cs Forwards FailureCause through which-node combined results.
Source/aweXpect.Core/Core/Nodes/OrNode.cs Forwards FailureCause through or-node combined results.
Source/aweXpect.Core/Core/Nodes/MappingNode.cs Forwards FailureCause through mapping-node combined results.
Source/aweXpect.Core/Core/Nodes/AsyncMappingNode.cs Forwards FailureCause through async-mapping combined results.
Source/aweXpect.Core/Core/Nodes/AndNode.cs Forwards FailureCause through and-node combined results.
Source/aweXpect.Core/Core/Initialization/AweXpectInitialization.cs Extends failure dispatch to call adapter overload when an inner exception exists; updates fallback adapter accordingly.
Source/aweXpect.Core/Core/Exceptions/FailException.cs Adds inner-exception constructor for fallback failure exception.
Source/aweXpect.Core/Core/Constraints/ConstraintResultExtensions.cs Forwards FailureCause through common ConstraintResult wrapper types.
Source/aweXpect.Core/Core/Constraints/ConstraintResult.FromException.cs Exposes FailureCause from exception-wrapped result.
Source/aweXpect.Core/Core/Constraints/ConstraintResult.cs Introduces FailureCause on the base result type.
Source/aweXpect.Core/Core/Adapters/ITestFrameworkAdapter.cs Adds Fail(message, innerException) contract to adapters.
Pipeline/Build.cs Changes pipeline build scope flag (currently set to CoreOnly).
Review details

Suppressed comments (2)

Tests/Frameworks/aweXpect.Frameworks.Tunit.Tests/TunitTestFrameworkTests.cs:23

  • The skip TUnit adapter test method name still mentions "Xunit2" and "SkipException", but the test asserts SkipTestException. Renaming keeps the method name aligned with the actual framework/exception type.
	[Test]
	public async Task OnFailWithCause_WhenUsingTUnitAsTestFramework_ShouldForwardTheCauseAsInnerException()
	{
		Exception cause = new InvalidOperationException("my cause");

Tests/Frameworks/aweXpect.Frameworks.Tunit.Tests/TunitTestFrameworkTests.cs:23

  • The inconclusive TUnit adapter test method name still mentions "Xunit2"/"XunitException" even though it asserts InconclusiveTestException. Renaming makes the intent and reported failures consistent with the TUnit framework.
	[Test]
	public async Task OnFailWithCause_WhenUsingTUnitAsTestFramework_ShouldForwardTheCauseAsInnerException()
	{
		Exception cause = new InvalidOperationException("my cause");
  • Files reviewed: 49/49 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread Pipeline/Build.cs
/// Afterward, you can update the package reference in `Directory.Packages.props` and reset this flag.
/// </summary>
readonly BuildScope BuildScope = BuildScope.Default;
readonly BuildScope BuildScope = BuildScope.CoreOnly;
Comment on lines 31 to 33
[Test]
public async Task OnInconclusive_WhenUsingNUnit3AsTestFramework_ShouldThrowAssertionException()
{
Comment on lines +20 to +23
[Test]
public async Task OnFailWithCause_WhenUsingTUnitAsTestFramework_ShouldForwardTheCauseAsInnerException()
{
Exception cause = new InvalidOperationException("my cause");
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

This is addressed in release v2.36.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change The changes require a new major version enhancement New feature or request state: released The issue is released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants