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)