From e4611f74e6ad548123ee8fdd3a1b5d9b04fbb534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 31 May 2026 08:37:45 +0200 Subject: [PATCH] feat: handle null/empty async `Because` reasons The synchronous `Because(string?)` overloads were already updated (#973) to ignore null or empty reasons. This applies the same behavior to the asynchronous `Because(Task)` overloads. The async overloads now accept `Task`, mirroring the nullable `string?` of the synchronous variants: the task reference is still required, but the value it resolves to may be null or empty. When the resolved value is null or empty, the reason is ignored (no `because` text is appended) instead of throwing a `NullReferenceException` (from `reason.Trim()`) or producing a dangling ", because " fragment. Because the resolved string is only available after awaiting, the null/empty guard lives in `AsyncBecauseReason.ApplyTo`, where the result is left unchanged when the reason resolves to null or empty. Changes: - `ExpectationResult.Because` and `ExpectationResult.Because` now take `Task`. - `ExpectationBuilder.AddReason` and `AsyncBecauseReason` updated to `Task`; the resolved value is checked for null/empty. - Regenerated the public-API approval files for all target frameworks. - Added tests covering null and empty resolved reasons for both the non-generic and generic async overloads. --- .../aweXpect.Core/Core/ExpectationBuilder.cs | 2 +- .../Core/Helpers/AsyncBecauseReason.cs | 10 ++++- .../Results/ExpectationResult.cs | 10 ++++- .../Expected/aweXpect.Core_net10.0.txt | 4 +- .../Expected/aweXpect.Core_net8.0.txt | 4 +- .../Expected/aweXpect.Core_netstandard2.0.txt | 4 +- .../aweXpect.Core.Tests/Core/BecauseTests.cs | 38 +++++++++++++++++++ 7 files changed, 61 insertions(+), 11 deletions(-) diff --git a/Source/aweXpect.Core/Core/ExpectationBuilder.cs b/Source/aweXpect.Core/Core/ExpectationBuilder.cs index 2c115b14b..c943a8916 100644 --- a/Source/aweXpect.Core/Core/ExpectationBuilder.cs +++ b/Source/aweXpect.Core/Core/ExpectationBuilder.cs @@ -309,7 +309,7 @@ internal void AddReason(string reason) /// /// Adds a to the current expectation constraint. /// - internal void AddReason(Task reason) + internal void AddReason(Task reason) { AsyncBecauseReason becauseReason = new(reason); _node.SetReason(becauseReason); diff --git a/Source/aweXpect.Core/Core/Helpers/AsyncBecauseReason.cs b/Source/aweXpect.Core/Core/Helpers/AsyncBecauseReason.cs index 6596d0c30..24724d672 100644 --- a/Source/aweXpect.Core/Core/Helpers/AsyncBecauseReason.cs +++ b/Source/aweXpect.Core/Core/Helpers/AsyncBecauseReason.cs @@ -4,7 +4,7 @@ namespace aweXpect.Core.Helpers; -internal struct AsyncBecauseReason(Task reason) : IBecauseReason +internal struct AsyncBecauseReason(Task reason) : IBecauseReason { private string? _message; @@ -27,7 +27,13 @@ public async Task { if (_message is null) { - _message = CreateMessage(await reason.ConfigureAwait(false)); + string? resolvedReason = await reason.ConfigureAwait(false); + if (string.IsNullOrEmpty(resolvedReason)) + { + return result; + } + + _message = CreateMessage(resolvedReason); } string message = _message; diff --git a/Source/aweXpect.Core/Results/ExpectationResult.cs b/Source/aweXpect.Core/Results/ExpectationResult.cs index bf78951f9..c6e660467 100644 --- a/Source/aweXpect.Core/Results/ExpectationResult.cs +++ b/Source/aweXpect.Core/Results/ExpectationResult.cs @@ -48,7 +48,10 @@ public ExpectationResult Because(string? reason) /// Provide an explaining why the constraint is needed.
/// If the phrase does not start with the word because, it is prepended automatically. /// - public ExpectationResult Because(Task reason) + /// + /// When the resolves to or empty, it is ignored. + /// + public ExpectationResult Because(Task reason) { expectationBuilder.AddReason(reason); return this; @@ -191,7 +194,10 @@ public TSelf Because(string? reason) /// Provide an explaining why the constraint is needed.
/// If the phrase does not start with the word because, it is prepended automatically. /// - public TSelf Because(Task reason) + /// + /// When the resolves to or empty, it is ignored. + /// + public TSelf Because(Task reason) { expectationBuilder.AddReason(reason); return (TSelf)this; diff --git a/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_net10.0.txt b/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_net10.0.txt index eea87999b..321fdca6a 100644 --- a/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_net10.0.txt +++ b/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_net10.0.txt @@ -1192,7 +1192,7 @@ namespace aweXpect.Results public class ExpectationResult : aweXpect.Results.Expectation, aweXpect.Core.IOptionsProvider { public ExpectationResult(aweXpect.Core.ExpectationBuilder expectationBuilder) { } - public aweXpect.Results.ExpectationResult Because(System.Threading.Tasks.Task reason) { } + public aweXpect.Results.ExpectationResult Because(System.Threading.Tasks.Task reason) { } public aweXpect.Results.ExpectationResult Because(string? reason) { } public System.Runtime.CompilerServices.TaskAwaiter GetAwaiter() { } public override string? ToString() { } @@ -1208,7 +1208,7 @@ namespace aweXpect.Results where TSelf : aweXpect.Results.ExpectationResult { public ExpectationResult(aweXpect.Core.ExpectationBuilder expectationBuilder) { } - public TSelf Because(System.Threading.Tasks.Task reason) { } + public TSelf Because(System.Threading.Tasks.Task reason) { } public TSelf Because(string? reason) { } [System.Diagnostics.StackTraceHidden] public System.Runtime.CompilerServices.TaskAwaiter GetAwaiter() { } diff --git a/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_net8.0.txt b/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_net8.0.txt index b489c8283..accb8bb30 100644 --- a/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_net8.0.txt +++ b/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_net8.0.txt @@ -1192,7 +1192,7 @@ namespace aweXpect.Results public class ExpectationResult : aweXpect.Results.Expectation, aweXpect.Core.IOptionsProvider { public ExpectationResult(aweXpect.Core.ExpectationBuilder expectationBuilder) { } - public aweXpect.Results.ExpectationResult Because(System.Threading.Tasks.Task reason) { } + public aweXpect.Results.ExpectationResult Because(System.Threading.Tasks.Task reason) { } public aweXpect.Results.ExpectationResult Because(string? reason) { } public System.Runtime.CompilerServices.TaskAwaiter GetAwaiter() { } public override string? ToString() { } @@ -1208,7 +1208,7 @@ namespace aweXpect.Results where TSelf : aweXpect.Results.ExpectationResult { public ExpectationResult(aweXpect.Core.ExpectationBuilder expectationBuilder) { } - public TSelf Because(System.Threading.Tasks.Task reason) { } + public TSelf Because(System.Threading.Tasks.Task reason) { } public TSelf Because(string? reason) { } [System.Diagnostics.StackTraceHidden] public System.Runtime.CompilerServices.TaskAwaiter GetAwaiter() { } diff --git a/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_netstandard2.0.txt b/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_netstandard2.0.txt index 3a784eb9c..bedddd98b 100644 --- a/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_netstandard2.0.txt +++ b/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_netstandard2.0.txt @@ -1155,7 +1155,7 @@ namespace aweXpect.Results public class ExpectationResult : aweXpect.Results.Expectation, aweXpect.Core.IOptionsProvider { public ExpectationResult(aweXpect.Core.ExpectationBuilder expectationBuilder) { } - public aweXpect.Results.ExpectationResult Because(System.Threading.Tasks.Task reason) { } + public aweXpect.Results.ExpectationResult Because(System.Threading.Tasks.Task reason) { } public aweXpect.Results.ExpectationResult Because(string? reason) { } public System.Runtime.CompilerServices.TaskAwaiter GetAwaiter() { } public override string? ToString() { } @@ -1170,7 +1170,7 @@ namespace aweXpect.Results where TSelf : aweXpect.Results.ExpectationResult { public ExpectationResult(aweXpect.Core.ExpectationBuilder expectationBuilder) { } - public TSelf Because(System.Threading.Tasks.Task reason) { } + public TSelf Because(System.Threading.Tasks.Task reason) { } public TSelf Because(string? reason) { } public System.Runtime.CompilerServices.TaskAwaiter GetAwaiter() { } public override string? ToString() { } diff --git a/Tests/aweXpect.Core.Tests/Core/BecauseTests.cs b/Tests/aweXpect.Core.Tests/Core/BecauseTests.cs index fb6013dd6..5c991e739 100644 --- a/Tests/aweXpect.Core.Tests/Core/BecauseTests.cs +++ b/Tests/aweXpect.Core.Tests/Core/BecauseTests.cs @@ -33,6 +33,23 @@ async Task Act() await That(Act).ThrowsException().WithMessage($"*{because}*").AsWildcard(); } + [Theory] + [InlineData(null)] + [InlineData("")] + public async Task ActionDelegate_WhenAsyncReasonIsNullOrEmpty_ShouldNotIncludeBecause(string? because) + { + Task becauseTask = Task.FromResult(because); + Action subject = () => throw new MyException(); + + async Task Act() + { + await That(subject).DoesNotThrow().Because(becauseTask); + } + + Exception exception = await That(Act).ThrowsException(); + await That(exception.Message).DoesNotContain("because"); + } + [Theory] [InlineData(null)] [InlineData("")] @@ -125,6 +142,27 @@ await That(subject).IsTrue().Because(because1) await That(Act).ThrowsException().WithMessage($"*{because1}*").AsWildcard(); } + [Theory] + [InlineData(null)] + [InlineData("")] + public async Task WhenAsyncReasonIsNullOrEmpty_ShouldNotIncludeBecause(string? because) + { + Task becauseTask = Task.FromResult(because); + bool subject = true; + + async Task Act() + { + await That(subject).IsFalse().Because(becauseTask); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + is False, + but it was True + """); + } + [Fact] public async Task WhenCombineWithAnd_ShouldApplyBecauseReason() {