From 2a62afc74bf801ca2e6b5ac86ddf25a72b1b42e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Breu=C3=9F=20Valentin?= Date: Wed, 29 Apr 2026 13:32:13 +0200 Subject: [PATCH 1/2] Combine extensions on verification results --- .../Verify/IVerificationResultExtensions.cs | 18 ------------- .../Verify/VerificationResultExtensions.cs | 26 ++++++++++++++++--- 2 files changed, 23 insertions(+), 21 deletions(-) delete mode 100644 Source/Mockolate/Verify/IVerificationResultExtensions.cs diff --git a/Source/Mockolate/Verify/IVerificationResultExtensions.cs b/Source/Mockolate/Verify/IVerificationResultExtensions.cs deleted file mode 100644 index d3aea5be..00000000 --- a/Source/Mockolate/Verify/IVerificationResultExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace Mockolate.Verify; - -internal static class IVerificationResultExtensions -{ - /// - /// Routes count terminators to the allocation-free fast path when the result implements - /// ; otherwise falls back to materialising the matching - /// interactions through and counting them. Lets - /// external implementers of remain whole-interface - /// implementable while still letting the framework's own results take the fast path. - /// - internal static bool VerifyCount(this IVerificationResult result, Func countPredicate) - => result is IFastVerifyCountResult fast - ? fast.VerifyCount(countPredicate) - : result.Verify(arr => countPredicate(arr.Length)); -} diff --git a/Source/Mockolate/Verify/VerificationResultExtensions.cs b/Source/Mockolate/Verify/VerificationResultExtensions.cs index 1f88167b..cc4a126d 100644 --- a/Source/Mockolate/Verify/VerificationResultExtensions.cs +++ b/Source/Mockolate/Verify/VerificationResultExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; using Mockolate.Exceptions; @@ -18,7 +19,7 @@ namespace Mockolate.Verify; /// interactions produced on a background thread. /// #if !DEBUG -[System.Diagnostics.DebuggerNonUserCode] +[DebuggerNonUserCode] #endif public static class VerificationResultExtensions { @@ -35,6 +36,21 @@ private static string ToTimes(this int amount, string verb = "") (_, _) => $"{verb} {amount} times", }; + extension(IVerificationResult result) + { + /// + /// Routes count terminators to the allocation-free fast path when the result implements + /// ; otherwise falls back to materialising the matching + /// interactions through and counting them. Lets + /// external implementers of remain whole-interface + /// implementable while still letting the framework's own results take the fast path. + /// + internal bool VerifyCount(Func countPredicate) + => result is IFastVerifyCountResult fast + ? fast.VerifyCount(countPredicate) + : result.Verify(arr => countPredicate(arr.Length)); + } + extension(VerificationResult verificationResult) { /// @@ -167,18 +183,21 @@ public void AtMost(int times) /// or when a timeout elapses first. /// /// - /// Thrown when is negative or is less than . + /// Thrown when is negative or is less than + /// . /// public void Between(int minimum, int maximum) { if (minimum < 0) { + // ReSharper disable once LocalizableElement throw new ArgumentOutOfRangeException(nameof(minimum), "Minimum value must be non-negative."); } if (maximum < minimum) { throw new ArgumentOutOfRangeException(nameof(maximum), + // ReSharper disable once LocalizableElement "Maximum value must be greater than or equal to minimum."); } @@ -387,7 +406,8 @@ public void Twice() /// Verifies that the mock was invoked according to the . /// /// - /// Receives the actual number of matching interactions and returns if that count is acceptable. + /// Receives the actual number of matching interactions and returns if that count is + /// acceptable. /// /// /// Populated by the compiler via From f86ff631f53378f4f8b8204eab29e8828bf47ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Breu=C3=9F=20Valentin?= Date: Wed, 29 Apr 2026 13:46:40 +0200 Subject: [PATCH 2/2] refactor: remove unused MockRegistry methods --- Source/Mockolate/MockRegistry.Interactions.cs | 49 +------------ .../Verify/VerificationResultExtensions.cs | 25 +++---- .../Expected/Mockolate_net10.0.txt | 2 - .../Expected/Mockolate_net8.0.txt | 2 - .../Expected/Mockolate_netstandard2.0.txt | 2 - Tests/Mockolate.Tests/MockRegistryTests.cs | 70 ++++++++++++++++++- .../RefStruct/RefStructStorageHelperTests.cs | 28 ++++++++ .../ItExtensionsTests.IsHttpContentTests.cs | 40 +++++++++++ ...tensionsTests.IsHttpRequestMessageTests.cs | 65 +++++++++++++++++ 9 files changed, 214 insertions(+), 69 deletions(-) create mode 100644 Tests/Mockolate.Tests/RefStruct/RefStructStorageHelperTests.cs diff --git a/Source/Mockolate/MockRegistry.Interactions.cs b/Source/Mockolate/MockRegistry.Interactions.cs index 89b35934..322306f0 100644 --- a/Source/Mockolate/MockRegistry.Interactions.cs +++ b/Source/Mockolate/MockRegistry.Interactions.cs @@ -450,54 +450,7 @@ public TResult GetProperty(PropertyGetterAccess access, Func d } /// - /// Member-id-keyed overload of that - /// records via the typed when the mock is wired to a - /// , falling back to the legacy list otherwise. - /// - /// The property's value type. - /// The generator-emitted member id for the property getter. - /// The simple property name. - /// Producer of the default value when no setup supplies one. - /// Optional accessor for the base-class getter; when only the default/initial value is considered. - /// The resolved getter value. - /// No setup exists for the property and is . - public TResult GetProperty(int memberId, string propertyName, Func defaultValueGenerator, - Func? baseValueAccessor) - { - if (!Behavior.SkipInteractionRecording) - { - RecordPropertyGetter(memberId, propertyName); - } - - return ResolveGetterInternal(propertyName, defaultValueGenerator, baseValueAccessor, null); - } - - /// - /// Singleton-aware overload of - /// that records the supplied directly. The source generator emits one shared - /// per non-indexer property; reusing it keeps every recorded access - /// in the matching pointing at the same singleton. - /// - /// The property's value type. - /// The generator-emitted member id for the property getter. - /// The shared singleton for the property. - /// Producer of the default value when no setup supplies one. - /// Optional accessor for the base-class getter; when only the default/initial value is considered. - /// The resolved getter value. - /// No setup exists for the property and is . - public TResult GetProperty(int memberId, PropertyGetterAccess access, - Func defaultValueGenerator, Func? baseValueAccessor) - { - if (!Behavior.SkipInteractionRecording) - { - RecordPropertyGetter(memberId, access); - } - - return ResolveGetterInternal(access.Name, defaultValueGenerator, baseValueAccessor, null); - } - - /// - /// Allocation-free fast-path overload of . + /// Allocation-free fast-path overload of . /// Avoids the per-call closure allocation by accepting a static that takes the /// active as its argument — the source generator emits a static lambda so /// the C# compiler caches the delegate in a static field. diff --git a/Source/Mockolate/Verify/VerificationResultExtensions.cs b/Source/Mockolate/Verify/VerificationResultExtensions.cs index cc4a126d..6e594cd9 100644 --- a/Source/Mockolate/Verify/VerificationResultExtensions.cs +++ b/Source/Mockolate/Verify/VerificationResultExtensions.cs @@ -36,20 +36,17 @@ private static string ToTimes(this int amount, string verb = "") (_, _) => $"{verb} {amount} times", }; - extension(IVerificationResult result) - { - /// - /// Routes count terminators to the allocation-free fast path when the result implements - /// ; otherwise falls back to materialising the matching - /// interactions through and counting them. Lets - /// external implementers of remain whole-interface - /// implementable while still letting the framework's own results take the fast path. - /// - internal bool VerifyCount(Func countPredicate) - => result is IFastVerifyCountResult fast - ? fast.VerifyCount(countPredicate) - : result.Verify(arr => countPredicate(arr.Length)); - } + /// + /// Routes count terminators to the allocation-free fast path when the result implements + /// ; otherwise falls back to materialising the matching + /// interactions through and counting them. Lets + /// external implementers of remain whole-interface + /// implementable while still letting the framework's own results take the fast path. + /// + internal static bool VerifyCount(this IVerificationResult result, Func countPredicate) + => result is IFastVerifyCountResult fast + ? fast.VerifyCount(countPredicate) + : result.Verify(arr => countPredicate(arr.Length)); extension(VerificationResult verificationResult) { diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt index ad654434..900b46a0 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt @@ -220,8 +220,6 @@ namespace Mockolate where T : Mockolate.Setup.MethodSetup { } public TResult GetProperty(Mockolate.Interactions.PropertyGetterAccess access, System.Func defaultValueGenerator, System.Func? baseValueAccessor) { } public TResult GetProperty(string propertyName, System.Func defaultValueGenerator, System.Func? baseValueAccessor) { } - public TResult GetProperty(int memberId, Mockolate.Interactions.PropertyGetterAccess access, System.Func defaultValueGenerator, System.Func? baseValueAccessor) { } - public TResult GetProperty(int memberId, string propertyName, System.Func defaultValueGenerator, System.Func? baseValueAccessor) { } public TResult GetPropertyFast(int memberId, Mockolate.Interactions.PropertyGetterAccess access, System.Func defaultValueGenerator, System.Func? baseValueAccessor = null) { } public TResult GetPropertyFast(int memberId, string propertyName, System.Func defaultValueGenerator, System.Func? baseValueAccessor = null) { } public Mockolate.Setup.PropertySetup? GetPropertySetupSnapshot(int memberId) { } diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt index 0af0a797..aca081f0 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt @@ -213,8 +213,6 @@ namespace Mockolate where T : Mockolate.Setup.MethodSetup { } public TResult GetProperty(Mockolate.Interactions.PropertyGetterAccess access, System.Func defaultValueGenerator, System.Func? baseValueAccessor) { } public TResult GetProperty(string propertyName, System.Func defaultValueGenerator, System.Func? baseValueAccessor) { } - public TResult GetProperty(int memberId, Mockolate.Interactions.PropertyGetterAccess access, System.Func defaultValueGenerator, System.Func? baseValueAccessor) { } - public TResult GetProperty(int memberId, string propertyName, System.Func defaultValueGenerator, System.Func? baseValueAccessor) { } public TResult GetPropertyFast(int memberId, Mockolate.Interactions.PropertyGetterAccess access, System.Func defaultValueGenerator, System.Func? baseValueAccessor = null) { } public TResult GetPropertyFast(int memberId, string propertyName, System.Func defaultValueGenerator, System.Func? baseValueAccessor = null) { } public Mockolate.Setup.PropertySetup? GetPropertySetupSnapshot(int memberId) { } diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt index 6d8e8eed..cade73e6 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt @@ -200,8 +200,6 @@ namespace Mockolate where T : Mockolate.Setup.MethodSetup { } public TResult GetProperty(Mockolate.Interactions.PropertyGetterAccess access, System.Func defaultValueGenerator, System.Func? baseValueAccessor) { } public TResult GetProperty(string propertyName, System.Func defaultValueGenerator, System.Func? baseValueAccessor) { } - public TResult GetProperty(int memberId, Mockolate.Interactions.PropertyGetterAccess access, System.Func defaultValueGenerator, System.Func? baseValueAccessor) { } - public TResult GetProperty(int memberId, string propertyName, System.Func defaultValueGenerator, System.Func? baseValueAccessor) { } public TResult GetPropertyFast(int memberId, Mockolate.Interactions.PropertyGetterAccess access, System.Func defaultValueGenerator, System.Func? baseValueAccessor = null) { } public TResult GetPropertyFast(int memberId, string propertyName, System.Func defaultValueGenerator, System.Func? baseValueAccessor = null) { } public Mockolate.Setup.PropertySetup? GetPropertySetupSnapshot(int memberId) { } diff --git a/Tests/Mockolate.Tests/MockRegistryTests.cs b/Tests/Mockolate.Tests/MockRegistryTests.cs index 1d37409e..1cb685f1 100644 --- a/Tests/Mockolate.Tests/MockRegistryTests.cs +++ b/Tests/Mockolate.Tests/MockRegistryTests.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; +using System.Reflection; using System.Threading; -using Mockolate.Interactions; using Mockolate.Setup; using Mockolate.Tests.TestHelpers; @@ -8,6 +8,34 @@ namespace Mockolate.Tests; public sealed class MockRegistryTests { + [Fact] + public async Task AddEvent_WithoutMemberIdAndMatchingSetup_ShouldInvokeSubscribedCallback() + { + MockRegistry registry = new(MockBehavior.Default, new FastMockInteractions(0)); + int subscribed = 0; + EventSetup setup = new(registry, "OnFoo"); + setup.OnSubscribed.Do(() => subscribed++); + registry.SetupEvent(setup); + + registry.AddEvent("OnFoo", this, GetMethodInfo()); + + await That(subscribed).IsEqualTo(1); + } + + [Fact] + public async Task GetProperty_WhenBaseValueAccessorThrows_ShouldRethrowException() + { + MockRegistry registry = new(MockBehavior.Default, new FastMockInteractions(0)); + InvalidOperationException expected = new("base failed"); + + void Act() + { + registry.GetProperty("Foo", () => 0, () => throw expected); + } + + await That(Act).Throws().WithMessage("base failed"); + } + [Fact] public async Task GetUnusedSetups_IndexerSetup_ShouldHaveCorrectString() { @@ -81,4 +109,44 @@ public async Task RegisterInteraction_ShouldBeThreadSafe() await That(sut.Interactions.Count).IsEqualTo(1000); } + + [Fact] + public async Task RemoveEvent_WithoutMemberIdAndMatchingSetup_ShouldInvokeUnsubscribedCallback() + { + MockRegistry registry = new(MockBehavior.Default, new FastMockInteractions(0)); + int unsubscribed = 0; + EventSetup setup = new(registry, "OnFoo"); + setup.OnUnsubscribed.Do(() => unsubscribed++); + registry.SetupEvent(setup); + + registry.RemoveEvent("OnFoo", this, GetMethodInfo()); + + await That(unsubscribed).IsEqualTo(1); + } + + [Fact] + public async Task SetProperty_WithMemberIdAndNoFastBuffer_ShouldRecordAndStore() + { + MockRegistry registry = new(MockBehavior.Default, new FastMockInteractions(0)); + + bool result = registry.SetProperty(7, "Foo", 42); + + await That(result).IsFalse(); + await That(registry.Interactions.Count).IsEqualTo(1); + } + + [Fact] + public async Task SetProperty_WithoutMemberId_SkippingBaseClass_ShouldReturnTrue() + { + MockBehavior behavior = MockBehavior.Default.SkippingBaseClass(); + MockRegistry registry = new(behavior, new FastMockInteractions(0, behavior.SkipInteractionRecording)); + + bool result = registry.SetProperty("Foo", 42); + + await That(result).IsTrue(); + } + + private static MethodInfo GetMethodInfo() + => typeof(MockRegistryTests).GetMethod(nameof(GetMethodInfo), + BindingFlags.Static | BindingFlags.NonPublic)!; } diff --git a/Tests/Mockolate.Tests/RefStruct/RefStructStorageHelperTests.cs b/Tests/Mockolate.Tests/RefStruct/RefStructStorageHelperTests.cs new file mode 100644 index 00000000..0e7f1d66 --- /dev/null +++ b/Tests/Mockolate.Tests/RefStruct/RefStructStorageHelperTests.cs @@ -0,0 +1,28 @@ +#if NET9_0_OR_GREATER +using Mockolate.Setup; + +namespace Mockolate.Tests.RefStruct; + +public sealed class RefStructStorageHelperTests +{ + [Fact] + public async Task TryResolveKey_WithNullProjectionAndNullRawKey_ShouldReturnFalse() + { + bool result = RefStructStorageHelper.TryResolveKey(null, 7, null, out object key); + + await That(result).IsFalse(); + await That(key).IsNull(); + } + + [Fact] + public async Task TryResolveKey_WithNullProjectionAndRawKey_ShouldReturnRawKey() + { + object boxed = 42; + + bool result = RefStructStorageHelper.TryResolveKey(null, 7, boxed, out object key); + + await That(result).IsTrue(); + await That(key).IsSameAs(boxed); + } +} +#endif diff --git a/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.cs b/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.cs index 2097138e..538eb2f9 100644 --- a/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.cs +++ b/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.cs @@ -200,6 +200,46 @@ await That(result.StatusCode) .IsEqualTo(expectSuccess ? HttpStatusCode.OK : HttpStatusCode.NotImplemented); } + [Fact] + public async Task Wrapper_NonGenericInvokeCallbacks_DelegatesToInnerParameter() + { + ItExtensions.IStringContentBodyMatchingParameter sut = It.IsHttpContent().WithStringMatching("foo*"); + int invocations = 0; + HttpContent? captured = new MyHttpContent(); + sut.Do(content => + { + invocations++; + captured = content; + }); + + sut.InvokeCallbacks(null); + + await That(invocations).IsEqualTo(1); + await That(captured).IsNull(); + } + + [Fact] + public async Task Wrapper_NonGenericMatches_DelegatesToInnerParameter() + { + ItExtensions.IStringContentBodyMatchingParameter sut = It.IsHttpContent().WithStringMatching("foo*"); + + bool resultForNull = sut.Matches(null); + bool resultForUnrelated = sut.Matches("not http content"); + + await That(resultForNull).IsFalse(); + await That(resultForUnrelated).IsFalse(); + } + + [Fact] + public async Task Wrapper_TypedMatches_ReturnsFalseForNullValue() + { + ItExtensions.IStringContentBodyMatchingParameter sut = It.IsHttpContent().WithStringMatching("foo*"); + + bool result = ((IParameterMatch)sut).Matches(null); + + await That(result).IsFalse(); + } + private sealed class MyHttpContent : HttpContent { protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) diff --git a/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpRequestMessageTests.cs b/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpRequestMessageTests.cs index 1a1ac08f..2f4ca30b 100644 --- a/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpRequestMessageTests.cs +++ b/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpRequestMessageTests.cs @@ -12,6 +12,51 @@ public sealed partial class ItExtensionsTests { public sealed partial class IsHttpRequestMessageTests { + [Fact] + public async Task NonGeneric_DispatchesHttpRequestMessageThroughCallback() + { + ItExtensions.IHttpRequestMessageParameter sut = It.IsHttpRequestMessage(); + HttpRequestMessage? captured = null; + sut.Do(message => captured = message); + + HttpRequestMessage target = new(HttpMethod.Get, "https://www.aweXpect.com"); + sut.InvokeCallbacks(target); + + await That(captured).IsSameAs(target); + } + + [Fact] + public async Task NonGeneric_IgnoresUnrelatedTypes() + { + ItExtensions.IHttpRequestMessageParameter sut = It.IsHttpRequestMessage(); + int invocations = 0; + sut.Do(_ => invocations++); + + sut.InvokeCallbacks("not a request message"); + + await That(invocations).IsEqualTo(0); + } + + [Fact] + public async Task NonGenericMatches_ReturnsFalseForUnrelatedType() + { + ItExtensions.IHttpRequestMessageParameter sut = It.IsHttpRequestMessage(); + + bool result = sut.Matches("not a request message"); + + await That(result).IsFalse(); + } + + [Fact] + public async Task NonGenericMatches_ReturnsTrueForHttpRequestMessage() + { + ItExtensions.IHttpRequestMessageParameter sut = It.IsHttpRequestMessage(); + + bool result = sut.Matches(new HttpRequestMessage(HttpMethod.Get, "https://www.aweXpect.com")); + + await That(result).IsTrue(); + } + [Fact] public async Task ShouldSupportMonitoring() { @@ -41,6 +86,26 @@ await That( await That(callbackCount).IsEqualTo(3); } + [Fact] + public async Task WithHeaders_OnRequestWithoutContent_ShouldStillEvaluate() + { + ItExtensions.IHttpRequestMessageParameter sut = It.IsHttpRequestMessage() + .WithHeaders(("x-correlation", "abc")); + HttpRequestMessage withoutContent = new(HttpMethod.Get, "https://www.aweXpect.com"); + withoutContent.Headers.Add("x-correlation", "abc"); + HttpRequestMessage withContent = new(HttpMethod.Post, "https://www.aweXpect.com") + { + Content = new StringContent("body"), + }; + withContent.Headers.Add("x-correlation", "abc"); + + bool matchesWithoutContent = ((IParameterMatch)sut).Matches(withoutContent); + bool matchesWithContent = ((IParameterMatch)sut).Matches(withContent); + + await That(matchesWithoutContent).IsTrue(); + await That(matchesWithContent).IsTrue(); + } + [Theory] [InlineData(nameof(HttpMethod.Get), true)] [InlineData(nameof(HttpMethod.Delete), false)]