From 20f782b71dda30af8f4b2d07c7d4fef024dcd34f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:37:48 +0000 Subject: [PATCH 1/4] Initial plan From 7b2c6445c3c6a36513a548e14352829ab5d3b355 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:43:48 +0000 Subject: [PATCH 2/4] Add FloatingPointAssertions, numeric comparison methods for IntegerAssertions, and Contain for EnumerableAssertions Co-authored-by: MrKWatkins <345796+MrKWatkins@users.noreply.github.com> --- .../EnumerableAssertionsTests.cs | 29 +++++ .../FloatingPointAssertionsTests.cs | 123 ++++++++++++++++++ .../IntegerAssertionsTests.cs | 80 ++++++++++++ .../EnumerableAssertions.cs | 14 ++ .../FloatingPointAssertions.cs | 73 +++++++++++ .../FloatingPointAssertionsChain.cs | 22 ++++ .../IntegerAssertions.cs | 48 +++++++ src/MrKWatkins.Assertions/ShouldExtensions.cs | 18 +++ 8 files changed, 407 insertions(+) create mode 100644 src/MrKWatkins.Assertions.Tests/FloatingPointAssertionsTests.cs create mode 100644 src/MrKWatkins.Assertions/FloatingPointAssertions.cs create mode 100644 src/MrKWatkins.Assertions/FloatingPointAssertionsChain.cs diff --git a/src/MrKWatkins.Assertions.Tests/EnumerableAssertionsTests.cs b/src/MrKWatkins.Assertions.Tests/EnumerableAssertionsTests.cs index 4369be5..986bcd9 100644 --- a/src/MrKWatkins.Assertions.Tests/EnumerableAssertionsTests.cs +++ b/src/MrKWatkins.Assertions.Tests/EnumerableAssertionsTests.cs @@ -168,4 +168,33 @@ public async Task NotSequenceEqual_Chain() await Assert.That(chain.Value).IsEqualTo(value); await Assert.That(chain.And.Value).IsEqualTo(value); } + + [Test] + public async Task Contain_Null() + { + IEnumerable nullEnumerable = null!; + + await Assert.That(() => nullEnumerable.Should().Contain(1)).Throws() + .WithMessage("Value should not be null."); + } + + [Test] + public async Task Contain() + { + var value = new List { 1, 2, 3 }; + + await Assert.That(() => value.Should().Contain(2)).ThrowsNothing(); + await Assert.That(() => value.Should().Contain(5)).Throws() + .WithMessage("Value should contain 5 but did not."); + } + + [Test] + public async Task Contain_Chain() + { + var value = new List { 1, 2, 3 }; + + var chain = value.Should().Contain(2); + await Assert.That(chain.Value).IsEqualTo(value); + await Assert.That(chain.And.Value).IsEqualTo(value); + } } \ No newline at end of file diff --git a/src/MrKWatkins.Assertions.Tests/FloatingPointAssertionsTests.cs b/src/MrKWatkins.Assertions.Tests/FloatingPointAssertionsTests.cs new file mode 100644 index 0000000..4ff1f1d --- /dev/null +++ b/src/MrKWatkins.Assertions.Tests/FloatingPointAssertionsTests.cs @@ -0,0 +1,123 @@ +namespace MrKWatkins.Assertions.Tests; + +public sealed class FloatingPointAssertionsTests +{ + [Test] + public async Task BeApproximately() + { + const double value = 17.5; + + await Assert.That(() => value.Should().BeApproximately(17.5, 0.1)).ThrowsNothing(); + await Assert.That(() => value.Should().BeApproximately(17.45, 0.1)).ThrowsNothing(); + await Assert.That(() => value.Should().BeApproximately(17.55, 0.1)).ThrowsNothing(); + await Assert.That(() => value.Should().BeApproximately(17.6, 0.1)).Throws() + .WithMessage("Value should be approximately 17.6 (±0.1) but was 17.5."); + await Assert.That(() => value.Should().BeApproximately(17.4, 0.1)).Throws() + .WithMessage("Value should be approximately 17.4 (±0.1) but was 17.5."); + } + + [Test] + public async Task BeApproximately_Float() + { + const float value = 17.5f; + + await Assert.That(() => value.Should().BeApproximately(17.5f, 0.1f)).ThrowsNothing(); + await Assert.That(() => value.Should().BeApproximately(17.6f, 0.05f)).Throws(); + } + + [Test] + public async Task BeApproximately_Chain() + { + const double value = 17.5; + + var chain = value.Should().BeApproximately(17.5, 0.1); + await Assert.That(chain.Value).IsEqualTo(value); + await Assert.That(chain.And.Value).IsEqualTo(value); + } + + [Test] + public async Task BeLessThan() + { + const double value = 5.0; + + await Assert.That(() => value.Should().BeLessThan(10.0)).ThrowsNothing(); + await Assert.That(() => value.Should().BeLessThan(5.0)).Throws() + .WithMessage("Value should be less than 5 but was 5."); + await Assert.That(() => value.Should().BeLessThan(1.0)).Throws() + .WithMessage("Value should be less than 1 but was 5."); + } + + [Test] + public async Task BeLessThan_Chain() + { + const double value = 5.0; + + var chain = value.Should().BeLessThan(10.0); + await Assert.That(chain.Value).IsEqualTo(value); + await Assert.That(chain.And.Value).IsEqualTo(value); + } + + [Test] + public async Task BeLessThanOrEqualTo() + { + const double value = 5.0; + + await Assert.That(() => value.Should().BeLessThanOrEqualTo(10.0)).ThrowsNothing(); + await Assert.That(() => value.Should().BeLessThanOrEqualTo(5.0)).ThrowsNothing(); + await Assert.That(() => value.Should().BeLessThanOrEqualTo(1.0)).Throws() + .WithMessage("Value should be less than or equal to 1 but was 5."); + } + + [Test] + public async Task BeLessThanOrEqualTo_Chain() + { + const double value = 5.0; + + var chain = value.Should().BeLessThanOrEqualTo(5.0); + await Assert.That(chain.Value).IsEqualTo(value); + await Assert.That(chain.And.Value).IsEqualTo(value); + } + + [Test] + public async Task BeGreaterThan() + { + const double value = 5.0; + + await Assert.That(() => value.Should().BeGreaterThan(1.0)).ThrowsNothing(); + await Assert.That(() => value.Should().BeGreaterThan(5.0)).Throws() + .WithMessage("Value should be greater than 5 but was 5."); + await Assert.That(() => value.Should().BeGreaterThan(10.0)).Throws() + .WithMessage("Value should be greater than 10 but was 5."); + } + + [Test] + public async Task BeGreaterThan_Chain() + { + const double value = 5.0; + + var chain = value.Should().BeGreaterThan(1.0); + await Assert.That(chain.Value).IsEqualTo(value); + await Assert.That(chain.And.Value).IsEqualTo(value); + } + + [Test] + public async Task BeGreaterThanOrEqualTo() + { + const double value = 5.0; + + await Assert.That(() => value.Should().BeGreaterThanOrEqualTo(1.0)).ThrowsNothing(); + await Assert.That(() => value.Should().BeGreaterThanOrEqualTo(5.0)).ThrowsNothing(); + await Assert.That(() => value.Should().BeGreaterThanOrEqualTo(10.0)).Throws() + .WithMessage("Value should be greater than or equal to 10 but was 5."); + } + + [Test] + public async Task BeGreaterThanOrEqualTo_Chain() + { + const double value = 5.0; + + var chain = value.Should().BeGreaterThanOrEqualTo(5.0); + await Assert.That(chain.Value).IsEqualTo(value); + await Assert.That(chain.And.Value).IsEqualTo(value); + } +} diff --git a/src/MrKWatkins.Assertions.Tests/IntegerAssertionsTests.cs b/src/MrKWatkins.Assertions.Tests/IntegerAssertionsTests.cs index 028feed..8da73ae 100644 --- a/src/MrKWatkins.Assertions.Tests/IntegerAssertionsTests.cs +++ b/src/MrKWatkins.Assertions.Tests/IntegerAssertionsTests.cs @@ -270,4 +270,84 @@ public async Task ChainWithInheritedMethods() await Assert.That(() => value.Should().BePositive().And.NotEqual(0)).ThrowsNothing(); await Assert.That(() => value.Should().NotBeZero().And.Equal(42)).ThrowsNothing(); } + + [Test] + public async Task BeLessThan() + { + const int value = 5; + + await Assert.That(() => value.Should().BeLessThan(10)).ThrowsNothing(); + await Assert.That(() => value.Should().BeLessThan(5)).Throws().WithMessage("Value should be less than 5 but was 5."); + await Assert.That(() => value.Should().BeLessThan(1)).Throws().WithMessage("Value should be less than 1 but was 5."); + } + + [Test] + public async Task BeLessThan_Chain() + { + const int value = 5; + + var chain = value.Should().BeLessThan(10); + await Assert.That(chain.Value).IsEqualTo(value); + await Assert.That(chain.And.Value).IsEqualTo(value); + } + + [Test] + public async Task BeLessThanOrEqualTo() + { + const int value = 5; + + await Assert.That(() => value.Should().BeLessThanOrEqualTo(10)).ThrowsNothing(); + await Assert.That(() => value.Should().BeLessThanOrEqualTo(5)).ThrowsNothing(); + await Assert.That(() => value.Should().BeLessThanOrEqualTo(1)).Throws().WithMessage("Value should be less than or equal to 1 but was 5."); + } + + [Test] + public async Task BeLessThanOrEqualTo_Chain() + { + const int value = 5; + + var chain = value.Should().BeLessThanOrEqualTo(5); + await Assert.That(chain.Value).IsEqualTo(value); + await Assert.That(chain.And.Value).IsEqualTo(value); + } + + [Test] + public async Task BeGreaterThan() + { + const int value = 5; + + await Assert.That(() => value.Should().BeGreaterThan(1)).ThrowsNothing(); + await Assert.That(() => value.Should().BeGreaterThan(5)).Throws().WithMessage("Value should be greater than 5 but was 5."); + await Assert.That(() => value.Should().BeGreaterThan(10)).Throws().WithMessage("Value should be greater than 10 but was 5."); + } + + [Test] + public async Task BeGreaterThan_Chain() + { + const int value = 5; + + var chain = value.Should().BeGreaterThan(1); + await Assert.That(chain.Value).IsEqualTo(value); + await Assert.That(chain.And.Value).IsEqualTo(value); + } + + [Test] + public async Task BeGreaterThanOrEqualTo() + { + const int value = 5; + + await Assert.That(() => value.Should().BeGreaterThanOrEqualTo(1)).ThrowsNothing(); + await Assert.That(() => value.Should().BeGreaterThanOrEqualTo(5)).ThrowsNothing(); + await Assert.That(() => value.Should().BeGreaterThanOrEqualTo(10)).Throws().WithMessage("Value should be greater than or equal to 10 but was 5."); + } + + [Test] + public async Task BeGreaterThanOrEqualTo_Chain() + { + const int value = 5; + + var chain = value.Should().BeGreaterThanOrEqualTo(5); + await Assert.That(chain.Value).IsEqualTo(value); + await Assert.That(chain.And.Value).IsEqualTo(value); + } } diff --git a/src/MrKWatkins.Assertions/EnumerableAssertions.cs b/src/MrKWatkins.Assertions/EnumerableAssertions.cs index 9305b69..d609d8d 100644 --- a/src/MrKWatkins.Assertions/EnumerableAssertions.cs +++ b/src/MrKWatkins.Assertions/EnumerableAssertions.cs @@ -128,4 +128,18 @@ public EnumerableAssertionsChain NotSequenceEqual(params IEnumer return new EnumerableAssertionsChain(this); } + + /// + /// Asserts that the enumerable contains the specified item. + /// + /// The item that should be present in the enumerable. + /// An for chaining further assertions. + public EnumerableAssertionsChain Contain(T expected) + { + NotBeNull(); + + Verify.That(Value.Contains(expected), $"Value should contain {expected} but did not."); + + return new EnumerableAssertionsChain(this); + } } \ No newline at end of file diff --git a/src/MrKWatkins.Assertions/FloatingPointAssertions.cs b/src/MrKWatkins.Assertions/FloatingPointAssertions.cs new file mode 100644 index 0000000..c1908b3 --- /dev/null +++ b/src/MrKWatkins.Assertions/FloatingPointAssertions.cs @@ -0,0 +1,73 @@ +using System.Numerics; + +namespace MrKWatkins.Assertions; + +/// +/// Provides assertions for floating-point values. +/// +/// The floating-point type of the value being asserted on. +/// The value to assert on. +public sealed class FloatingPointAssertions(T value) : ObjectAssertions(value) + where T : struct, IFloatingPoint +{ + /// + /// Asserts that the floating-point value is approximately equal to the expected value within the specified precision. + /// + /// The expected value. + /// The maximum allowed difference between the value and the expected value. + /// A for chaining further assertions. + public FloatingPointAssertionsChain BeApproximately(T expected, T precision) + { + Verify.That(T.Abs(Value - expected) <= precision, $"Value should be approximately {expected} (±{precision}) but was {Value}."); + + return new FloatingPointAssertionsChain(this); + } + + /// + /// Asserts that the floating-point value is less than the expected value. + /// + /// The value the floating-point number should be less than. + /// A for chaining further assertions. + public FloatingPointAssertionsChain BeLessThan(T expected) + { + Verify.That(Value < expected, $"Value should be less than {expected} but was {Value}."); + + return new FloatingPointAssertionsChain(this); + } + + /// + /// Asserts that the floating-point value is less than or equal to the expected value. + /// + /// The value the floating-point number should be less than or equal to. + /// A for chaining further assertions. + public FloatingPointAssertionsChain BeLessThanOrEqualTo(T expected) + { + Verify.That(Value <= expected, $"Value should be less than or equal to {expected} but was {Value}."); + + return new FloatingPointAssertionsChain(this); + } + + /// + /// Asserts that the floating-point value is greater than the expected value. + /// + /// The value the floating-point number should be greater than. + /// A for chaining further assertions. + public FloatingPointAssertionsChain BeGreaterThan(T expected) + { + Verify.That(Value > expected, $"Value should be greater than {expected} but was {Value}."); + + return new FloatingPointAssertionsChain(this); + } + + /// + /// Asserts that the floating-point value is greater than or equal to the expected value. + /// + /// The value the floating-point number should be greater than or equal to. + /// A for chaining further assertions. + public FloatingPointAssertionsChain BeGreaterThanOrEqualTo(T expected) + { + Verify.That(Value >= expected, $"Value should be greater than or equal to {expected} but was {Value}."); + + return new FloatingPointAssertionsChain(this); + } +} diff --git a/src/MrKWatkins.Assertions/FloatingPointAssertionsChain.cs b/src/MrKWatkins.Assertions/FloatingPointAssertionsChain.cs new file mode 100644 index 0000000..c73708a --- /dev/null +++ b/src/MrKWatkins.Assertions/FloatingPointAssertionsChain.cs @@ -0,0 +1,22 @@ +using System.Numerics; + +namespace MrKWatkins.Assertions; + +/// +/// Enables chaining of assertions on a floating-point value after a successful assertion. +/// +/// The floating-point type of the value being asserted on. +/// The assertions object to chain from. +public readonly struct FloatingPointAssertionsChain(FloatingPointAssertions floatingPointAssertions) + where T : struct, IFloatingPoint +{ + /// + /// Gets the assertions object for chaining further assertions. + /// + public FloatingPointAssertions And { get; } = floatingPointAssertions; + + /// + /// Gets the floating-point value being asserted on. + /// + public T Value => And.Value; +} diff --git a/src/MrKWatkins.Assertions/IntegerAssertions.cs b/src/MrKWatkins.Assertions/IntegerAssertions.cs index dc351c5..c67607c 100644 --- a/src/MrKWatkins.Assertions/IntegerAssertions.cs +++ b/src/MrKWatkins.Assertions/IntegerAssertions.cs @@ -133,4 +133,52 @@ public IntegerAssertionsChain NotBePositive() return new IntegerAssertionsChain(this); } + + /// + /// Asserts that the integer value is less than the expected value. + /// + /// The value the integer should be less than. + /// An for chaining further assertions. + public IntegerAssertionsChain BeLessThan(T expected) + { + Verify.That(Value < expected, $"Value should be less than {expected} but was {Value}."); + + return new IntegerAssertionsChain(this); + } + + /// + /// Asserts that the integer value is less than or equal to the expected value. + /// + /// The value the integer should be less than or equal to. + /// An for chaining further assertions. + public IntegerAssertionsChain BeLessThanOrEqualTo(T expected) + { + Verify.That(Value <= expected, $"Value should be less than or equal to {expected} but was {Value}."); + + return new IntegerAssertionsChain(this); + } + + /// + /// Asserts that the integer value is greater than the expected value. + /// + /// The value the integer should be greater than. + /// An for chaining further assertions. + public IntegerAssertionsChain BeGreaterThan(T expected) + { + Verify.That(Value > expected, $"Value should be greater than {expected} but was {Value}."); + + return new IntegerAssertionsChain(this); + } + + /// + /// Asserts that the integer value is greater than or equal to the expected value. + /// + /// The value the integer should be greater than or equal to. + /// An for chaining further assertions. + public IntegerAssertionsChain BeGreaterThanOrEqualTo(T expected) + { + Verify.That(Value >= expected, $"Value should be greater than or equal to {expected} but was {Value}."); + + return new IntegerAssertionsChain(this); + } } diff --git a/src/MrKWatkins.Assertions/ShouldExtensions.cs b/src/MrKWatkins.Assertions/ShouldExtensions.cs index e5b05e5..3637357 100644 --- a/src/MrKWatkins.Assertions/ShouldExtensions.cs +++ b/src/MrKWatkins.Assertions/ShouldExtensions.cs @@ -201,4 +201,22 @@ public static ReadOnlyDictionaryAssertions, TKey, TValu [Pure] [OverloadResolutionPriority(10)] public static IntegerAssertions Should(this nuint value) => new(value); + + /// + /// Begins a fluent assertion on the specified float value. + /// + /// The float value to assert on. + /// A for the value. + [Pure] + [OverloadResolutionPriority(10)] + public static FloatingPointAssertions Should(this float value) => new(value); + + /// + /// Begins a fluent assertion on the specified double value. + /// + /// The double value to assert on. + /// A for the value. + [Pure] + [OverloadResolutionPriority(10)] + public static FloatingPointAssertions Should(this double value) => new(value); } \ No newline at end of file From fd1d985770ad275d5765d11fcf80f5bae9e31948 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 01:12:25 +0000 Subject: [PATCH 3/4] Add AsyncActionAssertions with ThrowAsync/NotThrowAsync, Should(Func), and Awaiting extensions Co-authored-by: MrKWatkins <345796+MrKWatkins@users.noreply.github.com> --- .../AsyncActionAssertionsTests.cs | 137 ++++++++++++++++++ .../AsyncActionAssertions.cs | 70 +++++++++ .../InvokingExtensions.cs | 21 +++ src/MrKWatkins.Assertions/ShouldExtensions.cs | 8 + 4 files changed, 236 insertions(+) create mode 100644 src/MrKWatkins.Assertions.Tests/AsyncActionAssertionsTests.cs create mode 100644 src/MrKWatkins.Assertions/AsyncActionAssertions.cs diff --git a/src/MrKWatkins.Assertions.Tests/AsyncActionAssertionsTests.cs b/src/MrKWatkins.Assertions.Tests/AsyncActionAssertionsTests.cs new file mode 100644 index 0000000..3777fe8 --- /dev/null +++ b/src/MrKWatkins.Assertions.Tests/AsyncActionAssertionsTests.cs @@ -0,0 +1,137 @@ +namespace MrKWatkins.Assertions.Tests; + +public sealed class AsyncActionAssertionsTests +{ + [Test] + public async Task ThrowAsync() + { + Func doesNotThrow = () => Task.CompletedTask; + + var exception = new InvalidOperationException("Test"); + Func throws = () => throw exception; + + Func throwsWrongException = () => throw new NotSupportedException("Wrong"); + + await Assert.That(() => throws.Should().ThrowAsync()).ThrowsNothing(); + + await Assert.That(() => doesNotThrow.Should().ThrowAsync()) + .Throws().WithMessage("Function should throw an InvalidOperationException."); + + await Assert.That(() => throwsWrongException.Should().ThrowAsync()) + .Throws().WithMessage("Function should throw an InvalidOperationException but threw a NotSupportedException with message \"Wrong\"."); + } + + [Test] + public async Task ThrowAsync_Chain() + { + var exception = new InvalidOperationException("Test"); + Func throws = () => throw exception; + + var chain = await throws.Should().ThrowAsync().ConfigureAwait(false); + + await Assert.That(chain.Exception).IsSameReferenceAs(exception); + await Assert.That(chain.That).IsSameReferenceAs(exception); + } + + [Test] + public async Task ThrowAsync_String() + { + Func doesNotThrow = () => Task.CompletedTask; + + var exception = new InvalidOperationException("Test"); + Func throws = () => throw exception; + + Func throwsWrongException = () => throw new NotSupportedException("Wrong"); + + await Assert.That(() => throws.Should().ThrowAsync("Test")).ThrowsNothing(); + + await Assert.That(() => throws.Should().ThrowAsync("Wrong Message")) + .Throws().WithMessage("Value should have Message \"Wrong Message\" but was \"Test\"."); + + await Assert.That(() => doesNotThrow.Should().ThrowAsync("Test")) + .Throws().WithMessage("Function should throw an InvalidOperationException."); + + await Assert.That(() => throwsWrongException.Should().ThrowAsync("Test")) + .Throws().WithMessage("Function should throw an InvalidOperationException but threw a NotSupportedException with message \"Wrong\"."); + } + + [Test] + public async Task ThrowAsync_String_Chain() + { + var exception = new InvalidOperationException("Test"); + Func throws = () => throw exception; + + var chain = await throws.Should().ThrowAsync("Test").ConfigureAwait(false); + + await Assert.That(chain.Exception).IsSameReferenceAs(exception); + await Assert.That(chain.That).IsSameReferenceAs(exception); + } + + [Test] + public async Task NotThrowAsync() + { + Func doesNotThrow = () => Task.CompletedTask; + + var exception = new InvalidOperationException("Test"); + Func throws = () => throw exception; + + await Assert.That(() => doesNotThrow.Should().NotThrowAsync()).ThrowsNothing(); + + var actualException = await Assert.That(() => throws.Should().NotThrowAsync()) + .Throws() + .WithMessage("Function should not throw but threw an InvalidOperationException with message \"Test\"."); + await Assert.That(actualException!.InnerException).IsSameReferenceAs(exception); + } + + [Test] + public async Task ThrowAsync_ActuallyAsync() + { + var exception = new InvalidOperationException("AsyncTest"); + Func throwsAsync = async () => + { + await Task.Yield(); + throw exception; + }; + + var chain = await throwsAsync.Should().ThrowAsync("AsyncTest").ConfigureAwait(false); + await Assert.That(chain.Exception).IsSameReferenceAs(exception); + } + + [Test] + public async Task NotThrowAsync_ActuallyAsync() + { + Func doesNotThrow = async () => await Task.Yield(); + + await Assert.That(() => doesNotThrow.Should().NotThrowAsync()).ThrowsNothing(); + } + + [Test] + public async Task Awaiting_InvokingExtensions() + { + var value = new TestClass(); + + await Assert.That(() => value.Awaiting(v => v.ThrowAsync()).Should().ThrowAsync()) + .ThrowsNothing(); + + await Assert.That(() => value.Awaiting(v => v.NotThrowAsync()).Should().NotThrowAsync()) + .ThrowsNothing(); + } + + [Test] + public async Task Awaiting_InvokingExtensions_WithReturn() + { + var value = new TestClass(); + + await Assert.That(() => value.Awaiting(v => v.ThrowWithReturnAsync()).Should().ThrowAsync()) + .ThrowsNothing(); + } + + private sealed class TestClass + { + public Task ThrowAsync() => throw new InvalidOperationException(); + + public async Task NotThrowAsync() => await Task.Yield(); + + public Task ThrowWithReturnAsync() => throw new InvalidOperationException(); + } +} diff --git a/src/MrKWatkins.Assertions/AsyncActionAssertions.cs b/src/MrKWatkins.Assertions/AsyncActionAssertions.cs new file mode 100644 index 0000000..845679c --- /dev/null +++ b/src/MrKWatkins.Assertions/AsyncActionAssertions.cs @@ -0,0 +1,70 @@ +namespace MrKWatkins.Assertions; + +/// +/// Provides assertions for async actions (delegates returning ), such as verifying that exceptions are thrown. +/// +/// The async action to assert on. +public sealed class AsyncActionAssertions(Func action) +{ + /// + /// Asserts that the async action throws an exception of the specified type. + /// + /// The expected exception type. + /// A that resolves to an containing the thrown exception. + public async Task> ThrowAsync() + where TException : Exception + { + TException? thrown = null; + try + { + await action().ConfigureAwait(false); + } + catch (Exception exception) + { + if (exception is not TException typedException) + { + throw Verify.CreateException($"Function should throw {Format.PrefixWithAOrAn(typeof(TException).Name)} but threw {Format.PrefixWithAOrAn(exception.GetType().Name)} with message {Format.Value(exception.Message)}.", exception); + } + thrown = typedException; + } + + if (thrown == null) + { + throw Verify.CreateException($"Function should throw {Format.PrefixWithAOrAn(typeof(TException).Name)}."); + } + + return new ActionAssertionsChain(thrown); + } + + /// + /// Asserts that the async action throws an exception of the specified type with the specified message. + /// + /// The expected exception type. + /// The expected exception message. + /// A that resolves to an containing the thrown exception. + public async Task> ThrowAsync(string expectedMessage) + where TException : Exception + { + var chain = await ThrowAsync().ConfigureAwait(false); + + chain.That.Should().HaveMessage(expectedMessage); + + return chain; + } + + /// + /// Asserts that the async action does not throw any exception. + /// + /// A that completes successfully if no exception is thrown. + public async Task NotThrowAsync() + { + try + { + await action().ConfigureAwait(false); + } + catch (Exception exception) + { + throw Verify.CreateException($"Function should not throw but threw {Format.PrefixWithAOrAn(exception.GetType().Name)} with message {Format.Value(exception.Message)}.", exception); + } + } +} diff --git a/src/MrKWatkins.Assertions/InvokingExtensions.cs b/src/MrKWatkins.Assertions/InvokingExtensions.cs index 2a7e5cc..b70db3a 100644 --- a/src/MrKWatkins.Assertions/InvokingExtensions.cs +++ b/src/MrKWatkins.Assertions/InvokingExtensions.cs @@ -25,4 +25,25 @@ public static class InvokingExtensions /// An action that invokes the specified function with the value, for use with . [Pure] public static Action Invoking(this T value, Func action) => () => action(value); + + /// + /// Wraps an async action on the specified value for assertion testing. + /// + /// The type of the value. + /// The value to pass to the async action. + /// The async action to test. + /// A that invokes the specified async action with the value, for use with . + [Pure] + public static Func Awaiting(this T value, Func action) => () => action(value); + + /// + /// Wraps an async function on the specified value for assertion testing, discarding the return value. + /// + /// The type of the value. + /// The return type of the async function. + /// The value to pass to the async function. + /// The async function to test. + /// A that invokes the specified async function with the value, for use with . + [Pure] + public static Func Awaiting(this T value, Func> action) => () => action(value); } \ No newline at end of file diff --git a/src/MrKWatkins.Assertions/ShouldExtensions.cs b/src/MrKWatkins.Assertions/ShouldExtensions.cs index 3637357..dfa3685 100644 --- a/src/MrKWatkins.Assertions/ShouldExtensions.cs +++ b/src/MrKWatkins.Assertions/ShouldExtensions.cs @@ -112,6 +112,14 @@ public static ReadOnlyDictionaryAssertions, TKey, TValu [Pure] public static ActionAssertions Should([InstantHandle] this Action value) => new(value); + /// + /// Begins a fluent assertion on the specified async action. + /// + /// The async action to assert on. + /// An for the async action. + [Pure] + public static AsyncActionAssertions Should([InstantHandle] this Func value) => new(value); + /// /// Begins a fluent assertion on the specified byte value. /// From be0d89e59b49e5bcba13c4898c361a9a00553fd2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 09:55:54 +0000 Subject: [PATCH 4/4] Add precision to floating-point comparison methods; expand async action tests with async throw variants Co-authored-by: MrKWatkins <345796+MrKWatkins@users.noreply.github.com> --- .../AsyncActionAssertionsTests.cs | 54 +++++++++++-------- .../FloatingPointAssertionsTests.cs | 44 +++++++-------- .../FloatingPointAssertions.cs | 20 ++++--- 3 files changed, 67 insertions(+), 51 deletions(-) diff --git a/src/MrKWatkins.Assertions.Tests/AsyncActionAssertionsTests.cs b/src/MrKWatkins.Assertions.Tests/AsyncActionAssertionsTests.cs index 3777fe8..6ddd643 100644 --- a/src/MrKWatkins.Assertions.Tests/AsyncActionAssertionsTests.cs +++ b/src/MrKWatkins.Assertions.Tests/AsyncActionAssertionsTests.cs @@ -6,19 +6,27 @@ public sealed class AsyncActionAssertionsTests public async Task ThrowAsync() { Func doesNotThrow = () => Task.CompletedTask; + Func doesNotThrowAsync = async () => await Task.Yield(); var exception = new InvalidOperationException("Test"); Func throws = () => throw exception; + Func throwsAsync = async () => { await Task.Yield(); throw exception; }; Func throwsWrongException = () => throw new NotSupportedException("Wrong"); + Func throwsWrongExceptionAsync = async () => { await Task.Yield(); throw new NotSupportedException("Wrong"); }; await Assert.That(() => throws.Should().ThrowAsync()).ThrowsNothing(); + await Assert.That(() => throwsAsync.Should().ThrowAsync()).ThrowsNothing(); await Assert.That(() => doesNotThrow.Should().ThrowAsync()) .Throws().WithMessage("Function should throw an InvalidOperationException."); + await Assert.That(() => doesNotThrowAsync.Should().ThrowAsync()) + .Throws().WithMessage("Function should throw an InvalidOperationException."); await Assert.That(() => throwsWrongException.Should().ThrowAsync()) .Throws().WithMessage("Function should throw an InvalidOperationException but threw a NotSupportedException with message \"Wrong\"."); + await Assert.That(() => throwsWrongExceptionAsync.Should().ThrowAsync()) + .Throws().WithMessage("Function should throw an InvalidOperationException but threw a NotSupportedException with message \"Wrong\"."); } [Test] @@ -26,9 +34,13 @@ public async Task ThrowAsync_Chain() { var exception = new InvalidOperationException("Test"); Func throws = () => throw exception; + Func throwsAsync = async () => { await Task.Yield(); throw exception; }; var chain = await throws.Should().ThrowAsync().ConfigureAwait(false); + await Assert.That(chain.Exception).IsSameReferenceAs(exception); + await Assert.That(chain.That).IsSameReferenceAs(exception); + chain = await throwsAsync.Should().ThrowAsync().ConfigureAwait(false); await Assert.That(chain.Exception).IsSameReferenceAs(exception); await Assert.That(chain.That).IsSameReferenceAs(exception); } @@ -37,22 +49,32 @@ public async Task ThrowAsync_Chain() public async Task ThrowAsync_String() { Func doesNotThrow = () => Task.CompletedTask; + Func doesNotThrowAsync = async () => await Task.Yield(); var exception = new InvalidOperationException("Test"); Func throws = () => throw exception; + Func throwsAsync = async () => { await Task.Yield(); throw exception; }; Func throwsWrongException = () => throw new NotSupportedException("Wrong"); + Func throwsWrongExceptionAsync = async () => { await Task.Yield(); throw new NotSupportedException("Wrong"); }; await Assert.That(() => throws.Should().ThrowAsync("Test")).ThrowsNothing(); + await Assert.That(() => throwsAsync.Should().ThrowAsync("Test")).ThrowsNothing(); await Assert.That(() => throws.Should().ThrowAsync("Wrong Message")) .Throws().WithMessage("Value should have Message \"Wrong Message\" but was \"Test\"."); + await Assert.That(() => throwsAsync.Should().ThrowAsync("Wrong Message")) + .Throws().WithMessage("Value should have Message \"Wrong Message\" but was \"Test\"."); await Assert.That(() => doesNotThrow.Should().ThrowAsync("Test")) .Throws().WithMessage("Function should throw an InvalidOperationException."); + await Assert.That(() => doesNotThrowAsync.Should().ThrowAsync("Test")) + .Throws().WithMessage("Function should throw an InvalidOperationException."); await Assert.That(() => throwsWrongException.Should().ThrowAsync("Test")) .Throws().WithMessage("Function should throw an InvalidOperationException but threw a NotSupportedException with message \"Wrong\"."); + await Assert.That(() => throwsWrongExceptionAsync.Should().ThrowAsync("Test")) + .Throws().WithMessage("Function should throw an InvalidOperationException but threw a NotSupportedException with message \"Wrong\"."); } [Test] @@ -60,9 +82,13 @@ public async Task ThrowAsync_String_Chain() { var exception = new InvalidOperationException("Test"); Func throws = () => throw exception; + Func throwsAsync = async () => { await Task.Yield(); throw exception; }; var chain = await throws.Should().ThrowAsync("Test").ConfigureAwait(false); + await Assert.That(chain.Exception).IsSameReferenceAs(exception); + await Assert.That(chain.That).IsSameReferenceAs(exception); + chain = await throwsAsync.Should().ThrowAsync("Test").ConfigureAwait(false); await Assert.That(chain.Exception).IsSameReferenceAs(exception); await Assert.That(chain.That).IsSameReferenceAs(exception); } @@ -71,38 +97,24 @@ public async Task ThrowAsync_String_Chain() public async Task NotThrowAsync() { Func doesNotThrow = () => Task.CompletedTask; + Func doesNotThrowAsync = async () => await Task.Yield(); var exception = new InvalidOperationException("Test"); Func throws = () => throw exception; + Func throwsAsync = async () => { await Task.Yield(); throw exception; }; await Assert.That(() => doesNotThrow.Should().NotThrowAsync()).ThrowsNothing(); + await Assert.That(() => doesNotThrowAsync.Should().NotThrowAsync()).ThrowsNothing(); var actualException = await Assert.That(() => throws.Should().NotThrowAsync()) .Throws() .WithMessage("Function should not throw but threw an InvalidOperationException with message \"Test\"."); await Assert.That(actualException!.InnerException).IsSameReferenceAs(exception); - } - - [Test] - public async Task ThrowAsync_ActuallyAsync() - { - var exception = new InvalidOperationException("AsyncTest"); - Func throwsAsync = async () => - { - await Task.Yield(); - throw exception; - }; - - var chain = await throwsAsync.Should().ThrowAsync("AsyncTest").ConfigureAwait(false); - await Assert.That(chain.Exception).IsSameReferenceAs(exception); - } - [Test] - public async Task NotThrowAsync_ActuallyAsync() - { - Func doesNotThrow = async () => await Task.Yield(); - - await Assert.That(() => doesNotThrow.Should().NotThrowAsync()).ThrowsNothing(); + actualException = await Assert.That(() => throwsAsync.Should().NotThrowAsync()) + .Throws() + .WithMessage("Function should not throw but threw an InvalidOperationException with message \"Test\"."); + await Assert.That(actualException!.InnerException).IsSameReferenceAs(exception); } [Test] diff --git a/src/MrKWatkins.Assertions.Tests/FloatingPointAssertionsTests.cs b/src/MrKWatkins.Assertions.Tests/FloatingPointAssertionsTests.cs index 4ff1f1d..1f0921b 100644 --- a/src/MrKWatkins.Assertions.Tests/FloatingPointAssertionsTests.cs +++ b/src/MrKWatkins.Assertions.Tests/FloatingPointAssertionsTests.cs @@ -40,11 +40,11 @@ public async Task BeLessThan() { const double value = 5.0; - await Assert.That(() => value.Should().BeLessThan(10.0)).ThrowsNothing(); - await Assert.That(() => value.Should().BeLessThan(5.0)).Throws() - .WithMessage("Value should be less than 5 but was 5."); - await Assert.That(() => value.Should().BeLessThan(1.0)).Throws() - .WithMessage("Value should be less than 1 but was 5."); + await Assert.That(() => value.Should().BeLessThan(10.0, 0.001)).ThrowsNothing(); + await Assert.That(() => value.Should().BeLessThan(5.0, 0.001)).Throws() + .WithMessage("Value should be less than 5 (±0.001) but was 5."); + await Assert.That(() => value.Should().BeLessThan(1.0, 0.001)).Throws() + .WithMessage("Value should be less than 1 (±0.001) but was 5."); } [Test] @@ -52,7 +52,7 @@ public async Task BeLessThan_Chain() { const double value = 5.0; - var chain = value.Should().BeLessThan(10.0); + var chain = value.Should().BeLessThan(10.0, 0.001); await Assert.That(chain.Value).IsEqualTo(value); await Assert.That(chain.And.Value).IsEqualTo(value); } @@ -62,10 +62,10 @@ public async Task BeLessThanOrEqualTo() { const double value = 5.0; - await Assert.That(() => value.Should().BeLessThanOrEqualTo(10.0)).ThrowsNothing(); - await Assert.That(() => value.Should().BeLessThanOrEqualTo(5.0)).ThrowsNothing(); - await Assert.That(() => value.Should().BeLessThanOrEqualTo(1.0)).Throws() - .WithMessage("Value should be less than or equal to 1 but was 5."); + await Assert.That(() => value.Should().BeLessThanOrEqualTo(10.0, 0.001)).ThrowsNothing(); + await Assert.That(() => value.Should().BeLessThanOrEqualTo(5.0, 0.001)).ThrowsNothing(); + await Assert.That(() => value.Should().BeLessThanOrEqualTo(1.0, 0.001)).Throws() + .WithMessage("Value should be less than or equal to 1 (±0.001) but was 5."); } [Test] @@ -73,7 +73,7 @@ public async Task BeLessThanOrEqualTo_Chain() { const double value = 5.0; - var chain = value.Should().BeLessThanOrEqualTo(5.0); + var chain = value.Should().BeLessThanOrEqualTo(5.0, 0.001); await Assert.That(chain.Value).IsEqualTo(value); await Assert.That(chain.And.Value).IsEqualTo(value); } @@ -83,11 +83,11 @@ public async Task BeGreaterThan() { const double value = 5.0; - await Assert.That(() => value.Should().BeGreaterThan(1.0)).ThrowsNothing(); - await Assert.That(() => value.Should().BeGreaterThan(5.0)).Throws() - .WithMessage("Value should be greater than 5 but was 5."); - await Assert.That(() => value.Should().BeGreaterThan(10.0)).Throws() - .WithMessage("Value should be greater than 10 but was 5."); + await Assert.That(() => value.Should().BeGreaterThan(1.0, 0.001)).ThrowsNothing(); + await Assert.That(() => value.Should().BeGreaterThan(5.0, 0.001)).Throws() + .WithMessage("Value should be greater than 5 (±0.001) but was 5."); + await Assert.That(() => value.Should().BeGreaterThan(10.0, 0.001)).Throws() + .WithMessage("Value should be greater than 10 (±0.001) but was 5."); } [Test] @@ -95,7 +95,7 @@ public async Task BeGreaterThan_Chain() { const double value = 5.0; - var chain = value.Should().BeGreaterThan(1.0); + var chain = value.Should().BeGreaterThan(1.0, 0.001); await Assert.That(chain.Value).IsEqualTo(value); await Assert.That(chain.And.Value).IsEqualTo(value); } @@ -105,10 +105,10 @@ public async Task BeGreaterThanOrEqualTo() { const double value = 5.0; - await Assert.That(() => value.Should().BeGreaterThanOrEqualTo(1.0)).ThrowsNothing(); - await Assert.That(() => value.Should().BeGreaterThanOrEqualTo(5.0)).ThrowsNothing(); - await Assert.That(() => value.Should().BeGreaterThanOrEqualTo(10.0)).Throws() - .WithMessage("Value should be greater than or equal to 10 but was 5."); + await Assert.That(() => value.Should().BeGreaterThanOrEqualTo(1.0, 0.001)).ThrowsNothing(); + await Assert.That(() => value.Should().BeGreaterThanOrEqualTo(5.0, 0.001)).ThrowsNothing(); + await Assert.That(() => value.Should().BeGreaterThanOrEqualTo(10.0, 0.001)).Throws() + .WithMessage("Value should be greater than or equal to 10 (±0.001) but was 5."); } [Test] @@ -116,7 +116,7 @@ public async Task BeGreaterThanOrEqualTo_Chain() { const double value = 5.0; - var chain = value.Should().BeGreaterThanOrEqualTo(5.0); + var chain = value.Should().BeGreaterThanOrEqualTo(5.0, 0.001); await Assert.That(chain.Value).IsEqualTo(value); await Assert.That(chain.And.Value).IsEqualTo(value); } diff --git a/src/MrKWatkins.Assertions/FloatingPointAssertions.cs b/src/MrKWatkins.Assertions/FloatingPointAssertions.cs index c1908b3..0de0acc 100644 --- a/src/MrKWatkins.Assertions/FloatingPointAssertions.cs +++ b/src/MrKWatkins.Assertions/FloatingPointAssertions.cs @@ -27,10 +27,11 @@ public FloatingPointAssertionsChain BeApproximately(T expected, T precision) /// Asserts that the floating-point value is less than the expected value. /// /// The value the floating-point number should be less than. + /// The precision to use for the comparison. /// A for chaining further assertions. - public FloatingPointAssertionsChain BeLessThan(T expected) + public FloatingPointAssertionsChain BeLessThan(T expected, T precision) { - Verify.That(Value < expected, $"Value should be less than {expected} but was {Value}."); + Verify.That(Value < expected - precision, $"Value should be less than {expected} (±{precision}) but was {Value}."); return new FloatingPointAssertionsChain(this); } @@ -39,10 +40,11 @@ public FloatingPointAssertionsChain BeLessThan(T expected) /// Asserts that the floating-point value is less than or equal to the expected value. /// /// The value the floating-point number should be less than or equal to. + /// The precision to use for the comparison. /// A for chaining further assertions. - public FloatingPointAssertionsChain BeLessThanOrEqualTo(T expected) + public FloatingPointAssertionsChain BeLessThanOrEqualTo(T expected, T precision) { - Verify.That(Value <= expected, $"Value should be less than or equal to {expected} but was {Value}."); + Verify.That(Value <= expected + precision, $"Value should be less than or equal to {expected} (±{precision}) but was {Value}."); return new FloatingPointAssertionsChain(this); } @@ -51,10 +53,11 @@ public FloatingPointAssertionsChain BeLessThanOrEqualTo(T expected) /// Asserts that the floating-point value is greater than the expected value. /// /// The value the floating-point number should be greater than. + /// The precision to use for the comparison. /// A for chaining further assertions. - public FloatingPointAssertionsChain BeGreaterThan(T expected) + public FloatingPointAssertionsChain BeGreaterThan(T expected, T precision) { - Verify.That(Value > expected, $"Value should be greater than {expected} but was {Value}."); + Verify.That(Value > expected + precision, $"Value should be greater than {expected} (±{precision}) but was {Value}."); return new FloatingPointAssertionsChain(this); } @@ -63,10 +66,11 @@ public FloatingPointAssertionsChain BeGreaterThan(T expected) /// Asserts that the floating-point value is greater than or equal to the expected value. /// /// The value the floating-point number should be greater than or equal to. + /// The precision to use for the comparison. /// A for chaining further assertions. - public FloatingPointAssertionsChain BeGreaterThanOrEqualTo(T expected) + public FloatingPointAssertionsChain BeGreaterThanOrEqualTo(T expected, T precision) { - Verify.That(Value >= expected, $"Value should be greater than or equal to {expected} but was {Value}."); + Verify.That(Value >= expected - precision, $"Value should be greater than or equal to {expected} (±{precision}) but was {Value}."); return new FloatingPointAssertionsChain(this); }