From 39fe4bc7a25913a008477e24b5017bfcb2806de4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Mon, 6 Apr 2026 20:24:39 +0200 Subject: [PATCH 1/2] feat: add `It.IsNot` --- Source/Mockolate/It.IsNot.cs | 89 ++++++++++++++ Source/Mockolate/It.cs | 2 +- Tests/Mockolate.Tests/ItTests.IsNotTests.cs | 121 ++++++++++++++++++++ 3 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 Source/Mockolate/It.IsNot.cs create mode 100644 Tests/Mockolate.Tests/ItTests.IsNotTests.cs diff --git a/Source/Mockolate/It.IsNot.cs b/Source/Mockolate/It.IsNot.cs new file mode 100644 index 00000000..98e7718c --- /dev/null +++ b/Source/Mockolate/It.IsNot.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using Mockolate.Parameters; + +namespace Mockolate; + +#pragma warning disable S3453 // This class can't be instantiated; make its constructor 'public'. +#pragma warning disable S3218 // Inner class members should not shadow outer class "static" or type members +public partial class It +{ + /// + /// Matches a parameter that is not equal to . + /// + public static IIsNotParameter IsNot(T value, + [CallerArgumentExpression(nameof(value))] + string doNotPopulateThisValue = "") + => new ParameterEqualsNotMatch(value, doNotPopulateThisValue); + + /// + /// An used for equality comparison. + /// + public interface IIsNotParameter : IParameter + { + /// + /// Use the specified comparer to determine equality. + /// + IIsNotParameter Using(IEqualityComparer comparer, + [CallerArgumentExpression(nameof(comparer))] + string doNotPopulateThisValue = ""); + } + + [DebuggerNonUserCode] + private sealed class ParameterEqualsNotMatch : TypedMatch, IIsNotParameter + { + private readonly T _value; + private readonly string _valueExpression; + private IEqualityComparer? _comparer; + private string? _comparerExpression; + + public ParameterEqualsNotMatch(T value, string valueExpression) + { + _value = value; + _valueExpression = valueExpression; + } + + /// + public IIsNotParameter Using(IEqualityComparer comparer, string doNotPopulateThisValue = "") + { + _comparer = comparer; + _comparerExpression = doNotPopulateThisValue; + return this; + } + + /// + protected override bool Matches(T value) + { + if (_comparer is not null) + { + return !_comparer.Equals(value, _value); + } + + return !EqualityComparer.Default.Equals(value, _value); + } + + public override bool Matches(object? value) + { + if (value is T typedValue) + { + return Matches(typedValue); + } + + return Matches(default!); + } + + /// + public override string ToString() + { + if (_comparer is not null) + { + return $"It.IsNot({_valueExpression}).Using({_comparerExpression})"; + } + + return $"It.IsNot({_valueExpression})"; + } + } +} +#pragma warning restore S3218 // Inner class members should not shadow outer class "static" or type members +#pragma warning restore S3453 // This class can't be instantiated; make its constructor 'public'. diff --git a/Source/Mockolate/It.cs b/Source/Mockolate/It.cs index 479e449c..1aa9c1d5 100644 --- a/Source/Mockolate/It.cs +++ b/Source/Mockolate/It.cs @@ -56,7 +56,7 @@ private abstract class TypedMatch : IParameter, IParameter /// , if the is a matching parameter /// of type ; otherwise . /// - public bool Matches(object? value) + public virtual bool Matches(object? value) { if (value is T typedValue) { diff --git a/Tests/Mockolate.Tests/ItTests.IsNotTests.cs b/Tests/Mockolate.Tests/ItTests.IsNotTests.cs new file mode 100644 index 00000000..50097f52 --- /dev/null +++ b/Tests/Mockolate.Tests/ItTests.IsNotTests.cs @@ -0,0 +1,121 @@ +using Mockolate.Parameters; + +namespace Mockolate.Tests; + +public sealed partial class ItTests +{ + public sealed class IsNotTests + { + [Fact] + public async Task ShouldCorrectlyHandleNull() + { + IMyService sut = IMyService.CreateMock(); + MyImplementation value1 = new(); + sut.Mock.Setup.DoSomething(It.IsNot(null!)) + .Returns(3); + + int result1 = sut.DoSomething(value1); + int result2 = sut.DoSomething(null!); + + await That(result1).IsEqualTo(3); + await That(result2).IsEqualTo(0); + } + + [Theory] + [InlineData(1, true)] + [InlineData(5, false)] + [InlineData(-5, true)] + [InlineData(42, true)] + public async Task ShouldMatchWhenNotEqual(int value, bool expectMatch) + { + IParameter sut = It.IsNot(5); + + bool result = ((IParameter)sut).Matches(value); + + await That(result).IsEqualTo(expectMatch); + } + + [Fact] + public async Task ShouldSupportCovarianceInSetup() + { + IMyService sut = IMyService.CreateMock(); + MyImplementation value1 = new(); + MyOtherImplementation value2 = new(); + sut.Mock.Setup.DoSomething(It.IsNot(value1)) + .Returns(3); + + int result1 = sut.DoSomething(value1); + int result2 = sut.DoSomething(value2); + + await That(result1).IsEqualTo(0); + await That(result2).IsEqualTo(3); + } + + [Fact] + public async Task ToString_ShouldReturnExpectedValue() + { + IParameter sut = It.IsNot("foo"); + string expectedValue = "It.IsNot(\"foo\")"; + + string? result = sut.ToString(); + + await That(result).IsEqualTo(expectedValue); + } + + [Fact] + public async Task ToString_WithComparer_ShouldReturnExpectedValue() + { + IParameter sut = It.IsNot(4).Using(new AllEqualComparer()); + string expectedValue = "It.IsNot(4).Using(new AllEqualComparer())"; + + string? result = sut.ToString(); + + await That(result).IsEqualTo(expectedValue); + } + + [Theory] + [InlineData(1)] + [InlineData(5)] + [InlineData(-42)] + public async Task WithComparer_ShouldUseComparer(int value) + { + IParameter sut = It.IsNot(5).Using(new AllEqualComparer()); + + bool result = ((IParameter)sut).Matches(value); + + await That(result).IsFalse(); + } + + public interface IMyBase + { + int DoWork(); + } + + public class MyImplementation : IMyBase + { + public int Progress { get; private set; } + + public int DoWork() + { + Progress++; + return Progress; + } + } + + public class MyOtherImplementation : IMyBase + { + public string Output { get; private set; } = ""; + + public int DoWork() + { + Output += "did something\n"; + return 1; + } + } + + public interface IMyService + { + int DoSomething(IMyBase value); + } + } +} From 5087cf8188b99ef1317189a83f54180fbf4be304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Mon, 6 Apr 2026 20:40:04 +0200 Subject: [PATCH 2/2] Fix review issues --- Source/Mockolate/It.IsNot.cs | 4 ++-- .../Expected/Mockolate_net10.0.txt | 5 +++++ .../Expected/Mockolate_net8.0.txt | 5 +++++ .../Expected/Mockolate_netstandard2.0.txt | 5 +++++ Tests/Mockolate.Tests/ItTests.IsNotTests.cs | 15 +++++++++++++++ 5 files changed, 32 insertions(+), 2 deletions(-) diff --git a/Source/Mockolate/It.IsNot.cs b/Source/Mockolate/It.IsNot.cs index 98e7718c..d77e116e 100644 --- a/Source/Mockolate/It.IsNot.cs +++ b/Source/Mockolate/It.IsNot.cs @@ -62,7 +62,7 @@ protected override bool Matches(T value) return !EqualityComparer.Default.Equals(value, _value); } - + public override bool Matches(object? value) { if (value is T typedValue) @@ -70,7 +70,7 @@ public override bool Matches(object? value) return Matches(typedValue); } - return Matches(default!); + return value is not null || Matches(default!); } /// diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt index 4655390c..c4a900da 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt @@ -102,6 +102,7 @@ namespace Mockolate public static Mockolate.Parameters.IParameter IsFalse() { } public static Mockolate.It.IInRangeParameter IsInRange(T minimum, T maximum, [System.Runtime.CompilerServices.CallerArgumentExpression("minimum")] string doNotPopulateThisValue1 = "", [System.Runtime.CompilerServices.CallerArgumentExpression("maximum")] string doNotPopulateThisValue2 = "") where T : System.IComparable { } + public static Mockolate.It.IIsNotParameter IsNot(T value, [System.Runtime.CompilerServices.CallerArgumentExpression("value")] string doNotPopulateThisValue = "") { } public static Mockolate.Parameters.IParameter IsNotNull(string? toString = null) { } public static Mockolate.It.IIsNotOneOfParameter IsNotOneOf(System.Collections.Generic.IEnumerable values) { } public static Mockolate.Parameters.IParameter IsNull(string? toString = null) { } @@ -126,6 +127,10 @@ namespace Mockolate { Mockolate.It.IIsNotOneOfParameter Using(System.Collections.Generic.IEqualityComparer comparer, [System.Runtime.CompilerServices.CallerArgumentExpression("comparer")] string doNotPopulateThisValue = ""); } + public interface IIsNotParameter : Mockolate.Parameters.IParameter + { + Mockolate.It.IIsNotParameter Using(System.Collections.Generic.IEqualityComparer comparer, [System.Runtime.CompilerServices.CallerArgumentExpression("comparer")] string doNotPopulateThisValue = ""); + } public interface IIsOneOfParameter : Mockolate.Parameters.IParameter { Mockolate.It.IIsOneOfParameter Using(System.Collections.Generic.IEqualityComparer comparer, [System.Runtime.CompilerServices.CallerArgumentExpression("comparer")] string doNotPopulateThisValue = ""); diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt index 2e040193..acc48652 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt @@ -101,6 +101,7 @@ namespace Mockolate public static Mockolate.Parameters.IParameter IsFalse() { } public static Mockolate.It.IInRangeParameter IsInRange(T minimum, T maximum, [System.Runtime.CompilerServices.CallerArgumentExpression("minimum")] string doNotPopulateThisValue1 = "", [System.Runtime.CompilerServices.CallerArgumentExpression("maximum")] string doNotPopulateThisValue2 = "") where T : System.IComparable { } + public static Mockolate.It.IIsNotParameter IsNot(T value, [System.Runtime.CompilerServices.CallerArgumentExpression("value")] string doNotPopulateThisValue = "") { } public static Mockolate.Parameters.IParameter IsNotNull(string? toString = null) { } public static Mockolate.It.IIsNotOneOfParameter IsNotOneOf(System.Collections.Generic.IEnumerable values) { } public static Mockolate.Parameters.IParameter IsNull(string? toString = null) { } @@ -125,6 +126,10 @@ namespace Mockolate { Mockolate.It.IIsNotOneOfParameter Using(System.Collections.Generic.IEqualityComparer comparer, [System.Runtime.CompilerServices.CallerArgumentExpression("comparer")] string doNotPopulateThisValue = ""); } + public interface IIsNotParameter : Mockolate.Parameters.IParameter + { + Mockolate.It.IIsNotParameter Using(System.Collections.Generic.IEqualityComparer comparer, [System.Runtime.CompilerServices.CallerArgumentExpression("comparer")] string doNotPopulateThisValue = ""); + } public interface IIsOneOfParameter : Mockolate.Parameters.IParameter { Mockolate.It.IIsOneOfParameter Using(System.Collections.Generic.IEqualityComparer comparer, [System.Runtime.CompilerServices.CallerArgumentExpression("comparer")] string doNotPopulateThisValue = ""); diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt index 852c09c5..7343d1f4 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt @@ -90,6 +90,7 @@ namespace Mockolate public static Mockolate.Parameters.IParameter IsFalse() { } public static Mockolate.It.IInRangeParameter IsInRange(T minimum, T maximum, [System.Runtime.CompilerServices.CallerArgumentExpression("minimum")] string doNotPopulateThisValue1 = "", [System.Runtime.CompilerServices.CallerArgumentExpression("maximum")] string doNotPopulateThisValue2 = "") where T : System.IComparable { } + public static Mockolate.It.IIsNotParameter IsNot(T value, [System.Runtime.CompilerServices.CallerArgumentExpression("value")] string doNotPopulateThisValue = "") { } public static Mockolate.Parameters.IParameter IsNotNull(string? toString = null) { } public static Mockolate.It.IIsNotOneOfParameter IsNotOneOf(System.Collections.Generic.IEnumerable values) { } public static Mockolate.Parameters.IParameter IsNull(string? toString = null) { } @@ -112,6 +113,10 @@ namespace Mockolate { Mockolate.It.IIsNotOneOfParameter Using(System.Collections.Generic.IEqualityComparer comparer, [System.Runtime.CompilerServices.CallerArgumentExpression("comparer")] string doNotPopulateThisValue = ""); } + public interface IIsNotParameter : Mockolate.Parameters.IParameter + { + Mockolate.It.IIsNotParameter Using(System.Collections.Generic.IEqualityComparer comparer, [System.Runtime.CompilerServices.CallerArgumentExpression("comparer")] string doNotPopulateThisValue = ""); + } public interface IIsOneOfParameter : Mockolate.Parameters.IParameter { Mockolate.It.IIsOneOfParameter Using(System.Collections.Generic.IEqualityComparer comparer, [System.Runtime.CompilerServices.CallerArgumentExpression("comparer")] string doNotPopulateThisValue = ""); diff --git a/Tests/Mockolate.Tests/ItTests.IsNotTests.cs b/Tests/Mockolate.Tests/ItTests.IsNotTests.cs index 50097f52..801ec57c 100644 --- a/Tests/Mockolate.Tests/ItTests.IsNotTests.cs +++ b/Tests/Mockolate.Tests/ItTests.IsNotTests.cs @@ -21,6 +21,21 @@ public async Task ShouldCorrectlyHandleNull() await That(result2).IsEqualTo(0); } + [Fact] + public async Task ShouldCorrectlyHandleNullWithCovariance() + { + IMyService sut = IMyService.CreateMock(); + MyOtherImplementation value1 = new(); + sut.Mock.Setup.DoSomething(It.IsNot(null!)) + .Returns(3); + + int result1 = sut.DoSomething(value1); + int result2 = sut.DoSomething(null!); + + await That(result1).IsEqualTo(3); + await That(result2).IsEqualTo(0); + } + [Theory] [InlineData(1, true)] [InlineData(5, false)]