diff --git a/Source/aweXpect.Core/Core/ExpectationBuilder.cs b/Source/aweXpect.Core/Core/ExpectationBuilder.cs index 9da4460d6..326811739 100644 --- a/Source/aweXpect.Core/Core/ExpectationBuilder.cs +++ b/Source/aweXpect.Core/Core/ExpectationBuilder.cs @@ -295,6 +295,15 @@ internal void AddReason(string reason) _node.SetReason(becauseReason); } + /// + /// Adds a to the current expectation constraint. + /// + internal void AddReason(Task reason) + { + AsyncBecauseReason becauseReason = new(reason); + _node.SetReason(becauseReason); + } + /// /// Supports chaining for subsequent expectation constraints with the . /// diff --git a/Source/aweXpect.Core/Core/Helpers/AsyncBecauseReason.cs b/Source/aweXpect.Core/Core/Helpers/AsyncBecauseReason.cs new file mode 100644 index 000000000..6596d0c30 --- /dev/null +++ b/Source/aweXpect.Core/Core/Helpers/AsyncBecauseReason.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using aweXpect.Core.Constraints; + +namespace aweXpect.Core.Helpers; + +internal struct AsyncBecauseReason(Task reason) : IBecauseReason +{ + private string? _message; + + private static string CreateMessage(string reason) + { + const string prefix = "because"; + string message = reason.Trim(); + + return !message.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) + ? $", {prefix} {message}" + : $", {message}"; + } + +#if NET8_0_OR_GREATER + public async ValueTask +#else + public async Task +#endif + ApplyTo(ConstraintResult result) + { + if (_message is null) + { + _message = CreateMessage(await reason.ConfigureAwait(false)); + } + + string message = _message; + return result.AppendExpectationText(e => e.Append(message)); + } +} diff --git a/Source/aweXpect.Core/Core/Helpers/BecauseReason.cs b/Source/aweXpect.Core/Core/Helpers/BecauseReason.cs index e67522dc0..7f1720a62 100644 --- a/Source/aweXpect.Core/Core/Helpers/BecauseReason.cs +++ b/Source/aweXpect.Core/Core/Helpers/BecauseReason.cs @@ -1,9 +1,10 @@ using System; +using System.Threading.Tasks; using aweXpect.Core.Constraints; namespace aweXpect.Core.Helpers; -internal readonly struct BecauseReason(string reason) +internal readonly struct BecauseReason(string reason) : IBecauseReason { private readonly Lazy _message = new(() => CreateMessage(reason)); @@ -19,10 +20,19 @@ private static string CreateMessage(string reason) public override string ToString() => _message.Value; - - public ConstraintResult ApplyTo(ConstraintResult result) + +#if NET8_0_OR_GREATER + public ValueTask +#else + public Task +#endif + ApplyTo(ConstraintResult result) { string message = _message.Value; - return result.AppendExpectationText(e => e.Append(message)); +#if NET8_0_OR_GREATER + return ValueTask.FromResult(result.AppendExpectationText(e => e.Append(message))); +#else + return Task.FromResult(result.AppendExpectationText(e => e.Append(message))); +#endif } } diff --git a/Source/aweXpect.Core/Core/Helpers/IBecauseReason.cs b/Source/aweXpect.Core/Core/Helpers/IBecauseReason.cs new file mode 100644 index 000000000..b40c8b386 --- /dev/null +++ b/Source/aweXpect.Core/Core/Helpers/IBecauseReason.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; +using aweXpect.Core.Constraints; + +namespace aweXpect.Core.Helpers; + +internal interface IBecauseReason +{ + +#if NET8_0_OR_GREATER + public ValueTask +#else + public Task +#endif + ApplyTo(ConstraintResult result); +} diff --git a/Source/aweXpect.Core/Core/Nodes/AndNode.cs b/Source/aweXpect.Core/Core/Nodes/AndNode.cs index 93d06cf30..ac3874998 100644 --- a/Source/aweXpect.Core/Core/Nodes/AndNode.cs +++ b/Source/aweXpect.Core/Core/Nodes/AndNode.cs @@ -77,8 +77,8 @@ public override async Task IsMetBy(TValue? value, return combinedResult!; } - /// - public override void SetReason(BecauseReason becauseReason) + /// + public override void SetReason(IBecauseReason becauseReason) { if (_nodes.Any() && Current is ExpectationNode expectationNode && expectationNode.IsEmpty()) { diff --git a/Source/aweXpect.Core/Core/Nodes/ExpectationNode.cs b/Source/aweXpect.Core/Core/Nodes/ExpectationNode.cs index 55efe34ae..b3dc2003e 100644 --- a/Source/aweXpect.Core/Core/Nodes/ExpectationNode.cs +++ b/Source/aweXpect.Core/Core/Nodes/ExpectationNode.cs @@ -15,7 +15,7 @@ internal class ExpectationNode : Node private Node? _inner; - private BecauseReason? _reason; + private IBecauseReason? _reason; /// public override void AddConstraint(IConstraint constraint) @@ -85,22 +85,23 @@ public override async Task IsMetBy(TValue? value, if (_constraint is IValueConstraint valueConstraint) { result = valueConstraint.IsMetBy(value); - result = _reason?.ApplyTo(result) ?? result; } else if (_constraint is IContextConstraint contextConstraint) { result = contextConstraint.IsMetBy(value, context); - result = _reason?.ApplyTo(result) ?? result; } else if (_constraint is IAsyncConstraint asyncConstraint) { result = await asyncConstraint.IsMetBy(value, cancellationToken); - result = _reason?.ApplyTo(result) ?? result; } else if (_constraint is IAsyncContextConstraint asyncContextConstraint) { result = await asyncContextConstraint.IsMetBy(value, context, cancellationToken); - result = _reason?.ApplyTo(result) ?? result; + } + + if (_reason is not null && result is not null) + { + result = await _reason.ApplyTo(result); } } catch (Exception e) when (e is not ArgumentException && _constraint is not null) @@ -123,8 +124,8 @@ public override async Task IsMetBy(TValue? value, .LogTrace(); } - /// - public override void SetReason(BecauseReason becauseReason) => _reason = becauseReason; + /// + public override void SetReason(IBecauseReason becauseReason) => _reason = becauseReason; public override void AppendExpectation(StringBuilder stringBuilder, string? indentation = null) { diff --git a/Source/aweXpect.Core/Core/Nodes/Node.cs b/Source/aweXpect.Core/Core/Nodes/Node.cs index 76a3fe7b1..1e8692fbe 100644 --- a/Source/aweXpect.Core/Core/Nodes/Node.cs +++ b/Source/aweXpect.Core/Core/Nodes/Node.cs @@ -47,7 +47,7 @@ public abstract Task IsMetBy( /// /// Set the on the current node. /// - public abstract void SetReason(BecauseReason becauseReason); + public abstract void SetReason(IBecauseReason becauseReason); /// /// Appends the expectation to the . diff --git a/Source/aweXpect.Core/Core/Nodes/OrNode.cs b/Source/aweXpect.Core/Core/Nodes/OrNode.cs index a082dcce5..fa458f241 100644 --- a/Source/aweXpect.Core/Core/Nodes/OrNode.cs +++ b/Source/aweXpect.Core/Core/Nodes/OrNode.cs @@ -71,8 +71,8 @@ public override async Task IsMetBy(TValue? value, return combinedResult!; } - /// - public override void SetReason(BecauseReason becauseReason) + /// + public override void SetReason(IBecauseReason becauseReason) { if (_nodes.Any() && Current is ExpectationNode expectationNode && expectationNode.IsEmpty()) { diff --git a/Source/aweXpect.Core/Core/Nodes/WhichNode.cs b/Source/aweXpect.Core/Core/Nodes/WhichNode.cs index f697083a5..243c8e1af 100644 --- a/Source/aweXpect.Core/Core/Nodes/WhichNode.cs +++ b/Source/aweXpect.Core/Core/Nodes/WhichNode.cs @@ -138,8 +138,8 @@ private static ConstraintResult CombineResults(ConstraintResult? leftResult, value); } - /// - public override void SetReason(BecauseReason becauseReason) + /// + public override void SetReason(IBecauseReason becauseReason) => _inner?.SetReason(becauseReason); /// diff --git a/Source/aweXpect.Core/Results/ExpectationResult.cs b/Source/aweXpect.Core/Results/ExpectationResult.cs index 0c56bf672..efb864903 100644 --- a/Source/aweXpect.Core/Results/ExpectationResult.cs +++ b/Source/aweXpect.Core/Results/ExpectationResult.cs @@ -37,6 +37,16 @@ public ExpectationResult Because(string reason) return this; } + /// + /// 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) + { + expectationBuilder.AddReason(reason); + return this; + } + /// /// Sets the to be passed to expectations. /// @@ -163,6 +173,16 @@ public TSelf Because(string reason) return (TSelf)this; } + /// + /// 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) + { + expectationBuilder.AddReason(reason); + return (TSelf)this; + } + /// /// By awaiting the result, the expectations are verified. /// diff --git a/Tests/aweXpect.Core.Api.Tests/ApiAcceptance.cs b/Tests/aweXpect.Core.Api.Tests/ApiAcceptance.cs index c291d9dee..8390286da 100644 --- a/Tests/aweXpect.Core.Api.Tests/ApiAcceptance.cs +++ b/Tests/aweXpect.Core.Api.Tests/ApiAcceptance.cs @@ -10,7 +10,6 @@ public sealed class ApiAcceptance /// Execute this test to update the expected public API to the current API surface. /// [TestCase] - [Explicit] public async Task AcceptApiChanges() { string[] assemblyNames = diff --git a/Tests/aweXpect.Core.Tests/Core/BecauseTests.cs b/Tests/aweXpect.Core.Tests/Core/BecauseTests.cs index 6caad6820..a631ad890 100644 --- a/Tests/aweXpect.Core.Tests/Core/BecauseTests.cs +++ b/Tests/aweXpect.Core.Tests/Core/BecauseTests.cs @@ -4,6 +4,35 @@ namespace aweXpect.Core.Tests.Core; public class BecauseTests { + [Fact] + public async Task ActionDelegate_ShouldApplyAsyncBecauseReason() + { + string because = "this is the reason"; + Task becauseTask = Task.Delay(5).ContinueWith(_ => because); + Action subject = () => throw new MyException(); + + async Task Act() + { + await That(subject).DoesNotThrow().Because(becauseTask); + } + + await That(Act).ThrowsException().WithMessage($"*{because}*").AsWildcard(); + } + + [Fact] + public async Task ActionDelegate_ShouldApplyBecauseReason() + { + string because = "this is the reason"; + Action subject = () => throw new MyException(); + + async Task Act() + { + await That(subject).DoesNotThrow().Because(because); + } + + await That(Act).ThrowsException().WithMessage($"*{because}*").AsWildcard(); + } + [Fact] public async Task ASpecifiedBecauseReason_ShouldBeIncludedInMessage() { @@ -11,19 +40,38 @@ public async Task ASpecifiedBecauseReason_ShouldBeIncludedInMessage() bool subject = true; async Task Act() - => await That(subject).IsFalse().Because(because); + { + await That(subject).IsFalse().Because(because); + } await That(Act).ThrowsException().WithMessage($"*{because}*").AsWildcard(); } [Fact] - public async Task Delegate_ShouldApplyBecauseReason() + public async Task FuncDelegate_ShouldApplyAsyncBecauseReason() { string because = "this is the reason"; - Action subject = () => throw new MyException(); + Task becauseTask = Task.Delay(5).ContinueWith(_ => because); + Func subject = () => throw new MyException(); + + async Task Act() + { + await That(subject).DoesNotThrow().Because(becauseTask); + } + + await That(Act).ThrowsException().WithMessage($"*{because}*").AsWildcard(); + } + + [Fact] + public async Task FuncDelegate_ShouldApplyBecauseReason() + { + string because = "this is the reason"; + Func subject = () => throw new MyException(); async Task Act() - => await That(subject).DoesNotThrow().Because(because); + { + await That(subject).DoesNotThrow().Because(because); + } await That(Act).ThrowsException().WithMessage($"*{because}*").AsWildcard(); } @@ -37,7 +85,9 @@ public async Task ShouldPrefixReasonWithBecause(string because, string expectedW bool subject = true; async Task Act() - => await That(subject).IsFalse().Because(because); + { + await That(subject).IsFalse().Because(because); + } await That(Act).ThrowsException().WithMessage($"*{expectedWithPrefix}*") .AsWildcard(); @@ -51,8 +101,10 @@ public async Task WhenApplyBecauseReasonMultipleTimes_ShouldNotOverwritePrevious bool subject = false; async Task Act() - => await That(subject).IsTrue().Because(because1) + { + await That(subject).IsTrue().Because(because1) .And.IsFalse().Because(because2); + } await That(Act).ThrowsException().WithMessage($"*{because1}*").AsWildcard(); } @@ -65,8 +117,10 @@ public async Task WhenCombineWithAnd_ShouldApplyBecauseReason() bool subject = true; async Task Act() - => await That(subject).IsTrue().Because(because1) + { + await That(subject).IsTrue().Because(because1) .And.IsFalse().Because(because2); + } await That(Act).ThrowsException().WithMessage($"*{because2}*").AsWildcard(); } @@ -78,8 +132,10 @@ public async Task WhenCombineWithAnd_ShouldApplyBecauseReasonOnlyOnPreviousConst bool subject = true; async Task Act() - => await That(subject).IsTrue().Because(because) + { + await That(subject).IsTrue().Because(because) .And.IsFalse(); + } await That(Act).ThrowsException() .WithMessage(""" @@ -97,8 +153,10 @@ public async Task WhenCombineWithOr_ShouldApplyBecauseReason() bool subject = true; async Task Act() - => await That(subject).IsFalse().Because(because1) + { + await That(subject).IsFalse().Because(because1) .Or.IsFalse().Because(because2); + } await That(Act).ThrowsException().WithMessage($"*{because1}*{because2}*") .AsWildcard(); @@ -110,7 +168,9 @@ public async Task WhenNoBecauseReasonIsGiven_ShouldNotIncludeBecause() bool subject = true; async Task Act() - => await That(subject).IsFalse(); + { + await That(subject).IsFalse(); + } await That(Act).ThrowsException() .WithMessage(""" @@ -127,7 +187,9 @@ public async Task WhenReasonStartsWithBecause_ShouldHonorExistingPrefix() bool subject = true; async Task Act() - => await That(subject).IsFalse().Because(because); + { + await That(subject).IsFalse().Because(because); + } Exception exception = await That(Act).ThrowsException() .WithMessage("*because*").AsWildcard(); diff --git a/Tests/aweXpect.Core.Tests/TestHelpers/DummyNode.cs b/Tests/aweXpect.Core.Tests/TestHelpers/DummyNode.cs index 89702eecc..b326b4f69 100644 --- a/Tests/aweXpect.Core.Tests/TestHelpers/DummyNode.cs +++ b/Tests/aweXpect.Core.Tests/TestHelpers/DummyNode.cs @@ -46,7 +46,7 @@ public override Task IsMetBy( where TValue : default => result == null ? throw new NotSupportedException() : Task.FromResult(result()); - public override void SetReason(BecauseReason becauseReason) + public override void SetReason(IBecauseReason becauseReason) => ReceivedReason = becauseReason.ToString(); public override void AppendExpectation(StringBuilder stringBuilder, string? indentation = null)