diff --git a/Source/aweXpect.Core/Core/Polyfills/ReferenceEqualityComparer.cs b/Source/aweXpect.Core/Core/Polyfills/ReferenceEqualityComparer.cs index 2d07d0d3d..19cd2a4ff 100644 --- a/Source/aweXpect.Core/Core/Polyfills/ReferenceEqualityComparer.cs +++ b/Source/aweXpect.Core/Core/Polyfills/ReferenceEqualityComparer.cs @@ -2,6 +2,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; // ReSharper disable once CheckNamespace @@ -17,6 +18,7 @@ namespace System.Collections.Generic /// property /// to access the singleton instance of this type. /// + [ExcludeFromCodeCoverage] internal sealed class ReferenceEqualityComparer : IEqualityComparer, IEqualityComparer { private ReferenceEqualityComparer() { } diff --git a/Source/aweXpect/Equivalency/EquivalencyComparer.cs b/Source/aweXpect/Equivalency/EquivalencyComparer.cs index 26fc0068a..a430778b2 100644 --- a/Source/aweXpect/Equivalency/EquivalencyComparer.cs +++ b/Source/aweXpect/Equivalency/EquivalencyComparer.cs @@ -36,7 +36,7 @@ public string GetExtendedFailure(string it, ExpectationGrammars grammars, object { if (grammars.HasFlag(ExpectationGrammars.Negated)) { - return $"{it} was considered equivalent to {Formatter.Format(actual, FormattingOptions.Indented())}"; + return $"{it} was considered equivalent for {Formatter.Format(actual, FormattingOptions.Indented())}"; } if (actual is null != expected is null) @@ -50,14 +50,9 @@ public string GetExtendedFailure(string it, ExpectationGrammars grammars, object return _failureBuilder.ToString(); } - if (_failureBuilder.Length > 0) - { - _failureBuilder.Insert(0, " was not:"); - _failureBuilder.Insert(0, it); - return _failureBuilder.ToString(); - } - - return $"{it} was considered equivalent"; + _failureBuilder.Insert(0, " was not:"); + _failureBuilder.Insert(0, it); + return _failureBuilder.ToString(); } private static bool HandleSpecialCases(TActual actual, TExpected expected, @@ -69,6 +64,13 @@ private static bool HandleSpecialCases(TActual actual, TExpe isConsideredEqual = actualEqualityComparer.Equals(actual, expected); if (isConsideredEqual == false) { + failureBuilder.AppendLine(); + if (failureBuilder.Length > 2) + { + failureBuilder.AppendLine("and"); + } + + failureBuilder.Append(" "); Formatter.Format(failureBuilder, actual, FormattingOptions.SingleLine); failureBuilder.Append(" did not equal "); Formatter.Format(failureBuilder, expected, FormattingOptions.SingleLine); @@ -82,6 +84,13 @@ private static bool HandleSpecialCases(TActual actual, TExpe isConsideredEqual = expectedEqualityComparer.Equals(actual, expected); if (isConsideredEqual == false) { + failureBuilder.AppendLine(); + if (failureBuilder.Length > 2) + { + failureBuilder.AppendLine("and"); + } + + failureBuilder.Append(" "); Formatter.Format(failureBuilder, actual, FormattingOptions.SingleLine); failureBuilder.Append(" did not equal "); Formatter.Format(failureBuilder, expected, FormattingOptions.SingleLine); diff --git a/Source/aweXpect/That/Exceptions/ThatException.HasParamName.cs b/Source/aweXpect/That/Exceptions/ThatException.HasParamName.cs index 854ba77e0..192112091 100644 --- a/Source/aweXpect/That/Exceptions/ThatException.HasParamName.cs +++ b/Source/aweXpect/That/Exceptions/ThatException.HasParamName.cs @@ -42,20 +42,38 @@ public ConstraintResult IsMetBy(Exception? actual) protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) { - if (Grammars.HasFlag(ExpectationGrammars.Active)) + if (expected is null) { - stringBuilder.Append("with ParamName "); - } - else if (Grammars.HasFlag(ExpectationGrammars.Nested)) - { - stringBuilder.Append("ParamName is "); + if (Grammars.HasFlag(ExpectationGrammars.Active)) + { + stringBuilder.Append("with a ParamName"); + } + else if (Grammars.HasFlag(ExpectationGrammars.Nested)) + { + stringBuilder.Append("ParamName exists"); + } + else + { + stringBuilder.Append("has a ParamName"); + } } else { - stringBuilder.Append("has ParamName "); - } + if (Grammars.HasFlag(ExpectationGrammars.Active)) + { + stringBuilder.Append("with ParamName "); + } + else if (Grammars.HasFlag(ExpectationGrammars.Nested)) + { + stringBuilder.Append("ParamName is "); + } + else + { + stringBuilder.Append("has ParamName "); + } - Formatter.Format(stringBuilder, expected); + Formatter.Format(stringBuilder, expected); + } } protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) @@ -65,29 +83,42 @@ protected override void AppendNormalResult(StringBuilder stringBuilder, string? stringBuilder.Append(It).Append(" had ParamName "); Formatter.Format(stringBuilder, argumentException.ParamName); } - else - { - stringBuilder.Append(It).Append(" was "); - Formatter.Format(stringBuilder, Actual); - } } protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) { - if (Grammars.HasFlag(ExpectationGrammars.Active)) - { - stringBuilder.Append("without ParamName "); - } - else if (Grammars.HasFlag(ExpectationGrammars.Nested)) + if (expected is null) { - stringBuilder.Append("ParamName is not "); + if (Grammars.HasFlag(ExpectationGrammars.Active)) + { + stringBuilder.Append("without a ParamName"); + } + else if (Grammars.HasFlag(ExpectationGrammars.Nested)) + { + stringBuilder.Append("ParamName does not exist"); + } + else + { + stringBuilder.Append("does not have a ParamName"); + } } else { - stringBuilder.Append("does not have ParamName "); - } + if (Grammars.HasFlag(ExpectationGrammars.Active)) + { + stringBuilder.Append("without ParamName "); + } + else if (Grammars.HasFlag(ExpectationGrammars.Nested)) + { + stringBuilder.Append("ParamName is not "); + } + else + { + stringBuilder.Append("does not have ParamName "); + } - Formatter.Format(stringBuilder, expected); + Formatter.Format(stringBuilder, expected); + } } protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) diff --git a/Tests/aweXpect.Internal.Tests/ThatTests/Exceptions/HasParamNameValueConstraintTests.cs b/Tests/aweXpect.Internal.Tests/ThatTests/Exceptions/HasParamNameValueConstraintTests.cs new file mode 100644 index 000000000..04daf8b02 --- /dev/null +++ b/Tests/aweXpect.Internal.Tests/ThatTests/Exceptions/HasParamNameValueConstraintTests.cs @@ -0,0 +1,43 @@ +using System.Text; +using aweXpect.Core; + +namespace aweXpect.Internal.Tests.ThatTests.Exceptions; + +public class HasParamNameValueConstraintTests +{ + [Theory] + [InlineData(ExpectationGrammars.None, "has ParamName \"foo\"")] + [InlineData(ExpectationGrammars.Negated, "does not have ParamName \"foo\"")] + [InlineData(ExpectationGrammars.Nested, "ParamName is \"foo\"")] + [InlineData(ExpectationGrammars.Nested | ExpectationGrammars.Negated, "ParamName is not \"foo\"")] + [InlineData(ExpectationGrammars.Active, "with ParamName \"foo\"")] + [InlineData(ExpectationGrammars.Active | ExpectationGrammars.Negated, "without ParamName \"foo\"")] + public async Task AppendExpectation_WithExpected_ShouldAppendExpectedText(ExpectationGrammars grammar, + string expected) + { + ThatException.HasParamNameValueConstraint sut = new("it", grammar, "foo"); + StringBuilder sb = new(); + + sut.AppendExpectation(sb, ""); + + await That(sb.ToString()).IsEqualTo(expected); + } + + [Theory] + [InlineData(ExpectationGrammars.None, "has a ParamName")] + [InlineData(ExpectationGrammars.Negated, "does not have a ParamName")] + [InlineData(ExpectationGrammars.Nested, "ParamName exists")] + [InlineData(ExpectationGrammars.Nested | ExpectationGrammars.Negated, "ParamName does not exist")] + [InlineData(ExpectationGrammars.Active, "with a ParamName")] + [InlineData(ExpectationGrammars.Active | ExpectationGrammars.Negated, "without a ParamName")] + public async Task AppendExpectation_WithNullAsExpected_ShouldAppendExpectedText(ExpectationGrammars grammar, + string expected) + { + ThatException.HasParamNameValueConstraint sut = new("it", grammar, null); + StringBuilder sb = new(); + + sut.AppendExpectation(sb, ""); + + await That(sb.ToString()).IsEqualTo(expected); + } +} diff --git a/Tests/aweXpect.Tests/Exceptions/ThatException.HasParamName.Tests.cs b/Tests/aweXpect.Tests/Exceptions/ThatException.HasParamName.Tests.cs index 422cc4919..d8f591e3b 100644 --- a/Tests/aweXpect.Tests/Exceptions/ThatException.HasParamName.Tests.cs +++ b/Tests/aweXpect.Tests/Exceptions/ThatException.HasParamName.Tests.cs @@ -75,5 +75,91 @@ but it was """); } } + public sealed class NegatedTests + { + [Theory] + [AutoData] + public async Task WhenExpectedIsNull_AndParamNameIsEmpty_ShouldFail(string message) + { + ArgumentException subject = new(message); + + async Task Act() + => await That(subject).DoesNotComplyWith(it => + it.HasParamName(null)); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + does not have a ParamName, + but it had + """); + } + + [Theory] + [AutoData] + public async Task WhenExpectedIsNull_ShouldFail(string message) + { + ArgumentException subject = new(message, nameof(message)); + + async Task Act() + => await That(subject).DoesNotComplyWith(it => + it.HasParamName(null)); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + does not have a ParamName, + but it had + """); + } + + [Theory] + [AutoData] + public async Task WhenParamNameIsDifferent_ShouldSucceed(string message) + { + ArgumentException subject = new(message, nameof(message)); + + async Task Act() + => await That(subject).DoesNotComplyWith(it => + it.HasParamName("somethingElse")); + + await That(Act).DoesNotThrow(); + } + + [Theory] + [AutoData] + public async Task WhenParamNameMatchesExpected_ShouldFail(string message) + { + ArgumentException subject = new(message, nameof(message)); + + async Task Act() + => await That(subject).DoesNotComplyWith(it => + it.HasParamName("message")); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + does not have ParamName "message", + but it had + """); + } + + [Fact] + public async Task WhenSubjectIsNull_ShouldFail() + { + ArgumentException? subject = null; + + async Task Act() + => await That(subject).DoesNotComplyWith(it => + it.HasParamName("message")); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + does not have ParamName "message", + but it was + """); + } + } } } diff --git a/Tests/aweXpect.Tests/Objects/ThatObject.IsEquivalentTo.Tests.cs b/Tests/aweXpect.Tests/Objects/ThatObject.IsEquivalentTo.Tests.cs index 7e1999aea..8385ddcfc 100644 --- a/Tests/aweXpect.Tests/Objects/ThatObject.IsEquivalentTo.Tests.cs +++ b/Tests/aweXpect.Tests/Objects/ThatObject.IsEquivalentTo.Tests.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections; +using System.Collections.Generic; using System.Linq; using aweXpect.Equivalency; @@ -346,6 +347,62 @@ Property Inner.Inner.Value was instead of "Baz" - include public fields and properties """); } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task WhenActualImplementsIEqualityComparer_ShouldUseEqualityComparer(bool shouldBeEqual) + { + WithEqualityComparerToOuterClass subject = new(shouldBeEqual); + OuterClass expected = new() + { + Value = "Foo", + }; + + async Task Act() + => await That(subject).IsEquivalentTo(expected); + + await That(Act).Throws().OnlyIf(!shouldBeEqual) + .WithMessage(""" + Expected that subject + is equivalent to ThatObject.OuterClass { + Inner = , + Value = "Foo" + }, + but it was not: + ThatObject.IsEquivalentTo.WithEqualityComparerToOuterClass { } did not equal ThatObject.OuterClass { Inner = , Value = "Foo" } + + Equivalency options: + - include public fields and properties + """); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task WhenExpectedImplementsIEqualityComparer_ShouldUseEqualityComparer( + bool shouldBeEqual) + { + OuterClass subject = new() + { + Value = "Foo", + }; + WithEqualityComparerToOuterClass expected = new(shouldBeEqual); + + async Task Act() + => await That(subject).IsEquivalentTo(expected); + + await That(Act).Throws().OnlyIf(!shouldBeEqual) + .WithMessage(""" + Expected that subject + is equivalent to ThatObject.IsEquivalentTo.WithEqualityComparerToOuterClass { }, + but it was not: + ThatObject.OuterClass { Inner = , Value = "Foo" } did not equal ThatObject.IsEquivalentTo.WithEqualityComparerToOuterClass { } + + Equivalency options: + - include public fields and properties + """); + } } public sealed class CollectionTests @@ -491,7 +548,7 @@ Element [2] had superfluous 3 Expected: 3 and Element [4] was missing 4 - + Equivalency options: - include public fields and properties - ignore collection order @@ -545,7 +602,7 @@ is equivalent to { Element [3] differed: Found: 4 Expected: 5 - + Equivalency options: - include public fields and properties - ignore collection order @@ -584,7 +641,7 @@ is equivalent to { }, but it was not: Element [B] was missing - + Equivalency options: - include public fields and properties """); @@ -679,7 +736,7 @@ is equivalent to { }, but it was not: Element [B] had superfluous - + Equivalency options: - include public fields and properties """); @@ -1056,5 +1113,72 @@ private bool Equals(MyClassThrowingOnEqualsCheck? other) public override int GetHashCode() => Values != null ? Values.GetHashCode() : 0; } } + + public sealed class NegatedTests + { + [Fact] + public async Task EquivalentObjects_ShouldFail() + { + OuterClass subject = new() + { + Value = "Foo", + Inner = new InnerClass(), + }; + OuterClass expected = new() + { + Value = "Foo", + }; + + async Task Act() + => await That(subject).DoesNotComplyWith(it => + it.IsEquivalentTo(expected, o => o.IgnoringMember("Inner"))); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + is not equivalent to ThatObject.OuterClass { + Inner = , + Value = "Foo" + }, + but it was considered equivalent for ThatObject.OuterClass { + Inner = ThatObject.InnerClass { + Collection = , + Inner = , + Value = + }, + Value = "Foo" + } + + Equivalency options: + - include public fields and properties + - ignore members: ["Inner"] + """); + } + + [Fact] + public async Task MismatchedObjects_ShouldSucceed() + { + OuterClass subject = new(); + OuterClass expected = new() + { + Value = "Foo", + }; + + async Task Act() + => await That(subject).DoesNotComplyWith(it => + it.IsEquivalentTo(expected)); + + await That(Act).DoesNotThrow(); + } + } + + private sealed class WithEqualityComparerToOuterClass(bool shouldBeEqual) : IEqualityComparer + { + bool IEqualityComparer.Equals(object x, object y) + => shouldBeEqual; + + int IEqualityComparer.GetHashCode(object obj) + => obj.GetHashCode(); + } } } diff --git a/Tests/aweXpect.Tests/Objects/ThatObject.IsNotEquivalentTo.Tests.cs b/Tests/aweXpect.Tests/Objects/ThatObject.IsNotEquivalentTo.Tests.cs index 1ebeb0a28..f4988535c 100644 --- a/Tests/aweXpect.Tests/Objects/ThatObject.IsNotEquivalentTo.Tests.cs +++ b/Tests/aweXpect.Tests/Objects/ThatObject.IsNotEquivalentTo.Tests.cs @@ -31,7 +31,7 @@ is not equivalent to ThatObject.OuterClass { Inner = , Value = "Foo" }, - but it was considered equivalent to ThatObject.OuterClass { + but it was considered equivalent for ThatObject.OuterClass { Inner = , Value = "Foo" } @@ -112,7 +112,7 @@ is not equivalent to ThatObject.OuterClass { }, Value = "Foo" }, - but it was considered equivalent to ThatObject.OuterClass { + but it was considered equivalent for ThatObject.OuterClass { Inner = ThatObject.InnerClass { Collection = , Inner = ThatObject.InnerClass { @@ -220,7 +220,7 @@ is not equivalent to ThatObject.OuterClass { }, Value = "Foo" }, - but it was considered equivalent to ThatObject.OuterClass { + but it was considered equivalent for ThatObject.OuterClass { Inner = ThatObject.InnerClass { Collection = , Inner = ThatObject.InnerClass { @@ -290,7 +290,7 @@ is not equivalent to ThatObject.OuterClass { }, Value = "Foo" }, - but it was considered equivalent to ThatObject.OuterClass { + but it was considered equivalent for ThatObject.OuterClass { Inner = ThatObject.InnerClass { Collection = , Inner = ThatObject.InnerClass { @@ -424,7 +424,7 @@ is not equivalent to [ 3, 2 ], - but it was considered equivalent to [ + but it was considered equivalent for [ 1, 2, 3 @@ -453,7 +453,7 @@ is not equivalent to [ 2, 3 ], - but it was considered equivalent to [ + but it was considered equivalent for [ 1, 2, 3 @@ -568,7 +568,7 @@ is not equivalent to { [2] = 3, [1] = 4 }, - but it was considered equivalent to { + but it was considered equivalent for { [2] = 3, [1] = 4 } @@ -611,7 +611,7 @@ is not equivalent to { ["B"] = "B", ["A"] = "A" }, - but it was considered equivalent to { + but it was considered equivalent for { ["A"] = "A", ["B"] = "B" } diff --git a/Tests/aweXpect.Tests/Objects/ThatObject.IsNotOneOf.Tests.cs b/Tests/aweXpect.Tests/Objects/ThatObject.IsNotOneOf.Tests.cs index c79aa1fe9..27bfaaf7b 100644 --- a/Tests/aweXpect.Tests/Objects/ThatObject.IsNotOneOf.Tests.cs +++ b/Tests/aweXpect.Tests/Objects/ThatObject.IsNotOneOf.Tests.cs @@ -56,7 +56,7 @@ await That(Act).Throws() .WithMessage(""" Expected that subject is not equivalent to one of [ThatObject.MyClass { Value = 0 }], - but it was considered equivalent to ThatObject.MyClass { + but it was considered equivalent for ThatObject.MyClass { Value = 0 } """); diff --git a/Tests/aweXpect.Tests/ThatGeneric.DoesNotComplyWith.Tests.cs b/Tests/aweXpect.Tests/ThatGeneric.DoesNotComplyWith.Tests.cs index 116cf0db7..7546f3e81 100644 --- a/Tests/aweXpect.Tests/ThatGeneric.DoesNotComplyWith.Tests.cs +++ b/Tests/aweXpect.Tests/ThatGeneric.DoesNotComplyWith.Tests.cs @@ -83,7 +83,7 @@ await That(Act).Throws() .WithMessage(""" Expected that subject is not equivalent to { HasWaitedEnough = False } within 0:30, - but it was considered equivalent to ThatGeneric.DoesNotComplyWith.WithinTests.MyChangingClass { + but it was considered equivalent for ThatGeneric.DoesNotComplyWith.WithinTests.MyChangingClass { HasWaitedEnough = False } @@ -158,7 +158,7 @@ await That(Act).Throws() .WithMessage(""" Expected that subject is not equivalent to { HasWaitedEnough = False } within 0:00.050, - but it was considered equivalent to ThatGeneric.DoesNotComplyWith.WithinTests.MyChangingClass { + but it was considered equivalent for ThatGeneric.DoesNotComplyWith.WithinTests.MyChangingClass { HasWaitedEnough = False }