From 16fce4bad1bad9294536e9e63ae8755959cc89a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Wed, 22 Apr 2026 20:20:11 +0200 Subject: [PATCH 01/13] test: kill surviving indexer dispatch & ToString mutants Adds white-box tests against IndexerAccess / IndexerGetterAccess / IndexerSetterAccess that record which dispatch method (GetChildDispatch vs GetOrAddChildDispatch) TraverseStorage invokes for each parameter level, so a flipped createMissing conditional changes the observed sequence. Also pins the 4-parameter ToString output to catch the "null" -> "" string fallback mutations. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../IndexerAccessWhiteBoxTests.cs | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 Tests/Mockolate.Internal.Tests/IndexerAccessWhiteBoxTests.cs diff --git a/Tests/Mockolate.Internal.Tests/IndexerAccessWhiteBoxTests.cs b/Tests/Mockolate.Internal.Tests/IndexerAccessWhiteBoxTests.cs new file mode 100644 index 00000000..c503474a --- /dev/null +++ b/Tests/Mockolate.Internal.Tests/IndexerAccessWhiteBoxTests.cs @@ -0,0 +1,227 @@ +using System.Collections.Generic; +using Mockolate.Interactions; +using Mockolate.Setup; + +namespace Mockolate.Internal.Tests; + +public sealed class IndexerAccessWhiteBoxTests +{ + [Fact] + public async Task TryFindStoredValue_OnIndexerGetterAccess1_UsesGetChildDispatch() + { + RecordingStorage storage = new(); + IndexerGetterAccess access = new("p1", 42) { Storage = storage, }; + + access.TryFindStoredValue(out string _); + + await That(storage.Calls).IsEqualTo(["Get(42)",]); + } + + [Fact] + public async Task TryFindStoredValue_OnIndexerGetterAccess2_UsesGetChildDispatchForEachParameter() + { + RecordingStorage storage = new(); + IndexerGetterAccess access = new("p1", 1, "p2", 2) { Storage = storage, }; + + access.TryFindStoredValue(out string _); + + await That(storage.Calls).IsEqualTo(["Get(1)", "Get(2)",]); + } + + [Fact] + public async Task TryFindStoredValue_OnIndexerGetterAccess3_UsesGetChildDispatchForEachParameter() + { + RecordingStorage storage = new(); + IndexerGetterAccess access = new("p1", 1, "p2", 2, "p3", 3) { Storage = storage, }; + + access.TryFindStoredValue(out string _); + + await That(storage.Calls).IsEqualTo(["Get(1)", "Get(2)", "Get(3)",]); + } + + [Fact] + public async Task TryFindStoredValue_OnIndexerGetterAccess4_UsesGetChildDispatchForEachParameter() + { + RecordingStorage storage = new(); + IndexerGetterAccess access = + new("p1", 1, "p2", 2, "p3", 3, "p4", 4) { Storage = storage, }; + + access.TryFindStoredValue(out string _); + + await That(storage.Calls).IsEqualTo(["Get(1)", "Get(2)", "Get(3)", "Get(4)",]); + } + + [Fact] + public async Task StoreValue_OnIndexerGetterAccess1_UsesGetOrAddChildDispatch() + { + RecordingStorage storage = new(); + IndexerGetterAccess access = new("p1", 42) { Storage = storage, }; + + access.StoreValue("v"); + + await That(storage.Calls).IsEqualTo(["GetOrAdd(42)",]); + } + + [Fact] + public async Task StoreValue_OnIndexerGetterAccess2_UsesGetOrAddChildDispatchForEachParameter() + { + RecordingStorage storage = new(); + IndexerGetterAccess access = new("p1", 1, "p2", 2) { Storage = storage, }; + + access.StoreValue("v"); + + await That(storage.Calls).IsEqualTo(["GetOrAdd(1)", "GetOrAdd(2)",]); + } + + [Fact] + public async Task StoreValue_OnIndexerGetterAccess3_UsesGetOrAddChildDispatchForEachParameter() + { + RecordingStorage storage = new(); + IndexerGetterAccess access = new("p1", 1, "p2", 2, "p3", 3) { Storage = storage, }; + + access.StoreValue("v"); + + await That(storage.Calls).IsEqualTo(["GetOrAdd(1)", "GetOrAdd(2)", "GetOrAdd(3)",]); + } + + [Fact] + public async Task StoreValue_OnIndexerGetterAccess4_UsesGetOrAddChildDispatchForEachParameter() + { + RecordingStorage storage = new(); + IndexerGetterAccess access = + new("p1", 1, "p2", 2, "p3", 3, "p4", 4) { Storage = storage, }; + + access.StoreValue("v"); + + await That(storage.Calls).IsEqualTo(["GetOrAdd(1)", "GetOrAdd(2)", "GetOrAdd(3)", "GetOrAdd(4)",]); + } + + [Fact] + public async Task TryFindStoredValue_OnIndexerSetterAccess1_UsesGetChildDispatch() + { + RecordingStorage storage = new(); + IndexerSetterAccess access = new("p1", 42, "v") { Storage = storage, }; + + access.TryFindStoredValue(out string _); + + await That(storage.Calls).IsEqualTo(["Get(42)",]); + } + + [Fact] + public async Task TryFindStoredValue_OnIndexerSetterAccess2_UsesGetChildDispatchForEachParameter() + { + RecordingStorage storage = new(); + IndexerSetterAccess access = + new("p1", 1, "p2", 2, "v") { Storage = storage, }; + + access.TryFindStoredValue(out string _); + + await That(storage.Calls).IsEqualTo(["Get(1)", "Get(2)",]); + } + + [Fact] + public async Task TryFindStoredValue_OnIndexerSetterAccess3_UsesGetChildDispatchForEachParameter() + { + RecordingStorage storage = new(); + IndexerSetterAccess access = + new("p1", 1, "p2", 2, "p3", 3, "v") { Storage = storage, }; + + access.TryFindStoredValue(out string _); + + await That(storage.Calls).IsEqualTo(["Get(1)", "Get(2)", "Get(3)",]); + } + + [Fact] + public async Task TryFindStoredValue_OnIndexerSetterAccess4_UsesGetChildDispatchForEachParameter() + { + RecordingStorage storage = new(); + IndexerSetterAccess access = + new("p1", 1, "p2", 2, "p3", 3, "p4", 4, "v") { Storage = storage, }; + + access.TryFindStoredValue(out string _); + + await That(storage.Calls).IsEqualTo(["Get(1)", "Get(2)", "Get(3)", "Get(4)",]); + } + + [Fact] + public async Task StoreValue_OnIndexerSetterAccess1_UsesGetOrAddChildDispatch() + { + RecordingStorage storage = new(); + IndexerSetterAccess access = new("p1", 42, "v") { Storage = storage, }; + + access.StoreValue("v"); + + await That(storage.Calls).IsEqualTo(["GetOrAdd(42)",]); + } + + [Fact] + public async Task StoreValue_OnIndexerSetterAccess2_UsesGetOrAddChildDispatchForEachParameter() + { + RecordingStorage storage = new(); + IndexerSetterAccess access = + new("p1", 1, "p2", 2, "v") { Storage = storage, }; + + access.StoreValue("v"); + + await That(storage.Calls).IsEqualTo(["GetOrAdd(1)", "GetOrAdd(2)",]); + } + + [Fact] + public async Task StoreValue_OnIndexerSetterAccess3_UsesGetOrAddChildDispatchForEachParameter() + { + RecordingStorage storage = new(); + IndexerSetterAccess access = + new("p1", 1, "p2", 2, "p3", 3, "v") { Storage = storage, }; + + access.StoreValue("v"); + + await That(storage.Calls).IsEqualTo(["GetOrAdd(1)", "GetOrAdd(2)", "GetOrAdd(3)",]); + } + + [Fact] + public async Task StoreValue_OnIndexerSetterAccess4_UsesGetOrAddChildDispatchForEachParameter() + { + RecordingStorage storage = new(); + IndexerSetterAccess access = + new("p1", 1, "p2", 2, "p3", 3, "p4", 4, "v") { Storage = storage, }; + + access.StoreValue("v"); + + await That(storage.Calls).IsEqualTo(["GetOrAdd(1)", "GetOrAdd(2)", "GetOrAdd(3)", "GetOrAdd(4)",]); + } + + [Fact] + public async Task ToString_OnIndexerGetterAccess4_FormatsEachNullParameterAsNullLiteral() + { + IndexerGetterAccess access = + new("p1", null, "p2", null, "p3", null, "p4", null); + + await That(access.ToString()).IsEqualTo("get indexer [null, null, null, null]"); + } + + [Fact] + public async Task ToString_OnIndexerSetterAccess4_FormatsEachNullParameterAsNullLiteral() + { + IndexerSetterAccess access = + new("p1", null, "p2", null, "p3", null, "p4", null, null); + + await That(access.ToString()).IsEqualTo("set indexer [null, null, null, null] to null"); + } + + private sealed class RecordingStorage : IndexerValueStorage + { + public List Calls { get; } = []; + + public override IndexerValueStorage? GetChildDispatch(TKey key) + { + Calls.Add($"Get({key?.ToString() ?? ""})"); + return this; + } + + public override IndexerValueStorage GetOrAddChildDispatch(TKey key) + { + Calls.Add($"GetOrAdd({key?.ToString() ?? ""})"); + return this; + } + } +} From 63f52f1cdb9168f274b8cc5dc398277f4957dd75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Wed, 22 Apr 2026 20:46:47 +0200 Subject: [PATCH 02/13] test: kill indexer setup TryCast, StoreValue, and InParallel mutants Pins IndexerSetup.TryCast null handling, verifies StoreValue caches the computed result inside each GetResult overload so subsequent reads see it, and covers TransitionTo's InParallel flag by chaining it after a serial OnGet/OnSet.Do callback: the scenario must still transition even when the preceding callback already exclusively handled the access. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../IndexerSetupMutationTests.cs | 131 ++++++++++++++++++ .../TestHelpers/FakeIndexerSetup.cs | 3 + ...ndexerTests.TransitionToInParallelTests.cs | 129 +++++++++++++++++ 3 files changed, 263 insertions(+) create mode 100644 Tests/Mockolate.Internal.Tests/IndexerSetupMutationTests.cs create mode 100644 Tests/Mockolate.Tests/MockIndexers/SetupIndexerTests.TransitionToInParallelTests.cs diff --git a/Tests/Mockolate.Internal.Tests/IndexerSetupMutationTests.cs b/Tests/Mockolate.Internal.Tests/IndexerSetupMutationTests.cs new file mode 100644 index 00000000..c58e98f9 --- /dev/null +++ b/Tests/Mockolate.Internal.Tests/IndexerSetupMutationTests.cs @@ -0,0 +1,131 @@ +using Mockolate.Interactions; +using Mockolate.Internal.Tests.TestHelpers; +using Mockolate.Parameters; +using Mockolate.Setup; + +namespace Mockolate.Internal.Tests; + +public sealed class IndexerSetupMutationTests +{ + [Fact] + public async Task TryCast_WhenValueIsNull_ShouldReturnTrue() + { + bool success = FakeIndexerSetup.InvokeTryCast(null, out string result, MockBehavior.Default); + + await That(success).IsTrue(); + await That(result).IsNull(); + } + + [Fact] + public async Task TryCast_WhenValueIsNotOfTargetTypeAndNotNull_ShouldReturnFalse() + { + bool success = FakeIndexerSetup.InvokeTryCast(42, out string _, MockBehavior.Default); + + await That(success).IsFalse(); + } + + [Fact] + public async Task GetResult_WithBaseValue_1Param_StoresComputedValueForLaterLookup() + { + IndexerSetup setup = new( + new MockRegistry(MockBehavior.Default), + (IParameterMatch)It.IsAny()); + IndexerValueStorage storage = new(); + IndexerGetterAccess access1 = new("p", 42) { Storage = storage, }; + + string result = setup.GetResult(access1, MockBehavior.Default, "base"); + + IndexerGetterAccess access2 = new("p", 42) { Storage = storage, }; + bool found = access2.TryFindStoredValue(out string stored); + + await That(result).IsEqualTo("base"); + await That(found).IsTrue(); + await That(stored).IsEqualTo("base"); + } + + [Fact] + public async Task GetResult_WithDefaultValueGenerator_1Param_StoresComputedValueForLaterLookup() + { + IndexerSetup setup = new( + new MockRegistry(MockBehavior.Default), + (IParameterMatch)It.IsAny()); + IndexerValueStorage storage = new(); + IndexerGetterAccess access1 = new("p", 42) { Storage = storage, }; + + string result = setup.GetResult(access1, MockBehavior.Default, () => "generated"); + + IndexerGetterAccess access2 = new("p", 42) { Storage = storage, }; + bool found = access2.TryFindStoredValue(out string stored); + + await That(result).IsEqualTo("generated"); + await That(found).IsTrue(); + await That(stored).IsEqualTo("generated"); + } + + [Fact] + public async Task GetResult_WithBaseValue_2Param_StoresComputedValueForLaterLookup() + { + IndexerSetup setup = new( + new MockRegistry(MockBehavior.Default), + (IParameterMatch)It.IsAny(), + (IParameterMatch)It.IsAny()); + IndexerValueStorage storage = new(); + IndexerGetterAccess access1 = new("p1", 1, "p2", 2) { Storage = storage, }; + + string result = setup.GetResult(access1, MockBehavior.Default, "base"); + + IndexerGetterAccess access2 = new("p1", 1, "p2", 2) { Storage = storage, }; + bool found = access2.TryFindStoredValue(out string stored); + + await That(result).IsEqualTo("base"); + await That(found).IsTrue(); + await That(stored).IsEqualTo("base"); + } + + [Fact] + public async Task GetResult_WithBaseValue_3Param_StoresComputedValueForLaterLookup() + { + IndexerSetup setup = new( + new MockRegistry(MockBehavior.Default), + (IParameterMatch)It.IsAny(), + (IParameterMatch)It.IsAny(), + (IParameterMatch)It.IsAny()); + IndexerValueStorage storage = new(); + IndexerGetterAccess access1 = + new("p1", 1, "p2", 2, "p3", 3) { Storage = storage, }; + + string result = setup.GetResult(access1, MockBehavior.Default, "base"); + + IndexerGetterAccess access2 = + new("p1", 1, "p2", 2, "p3", 3) { Storage = storage, }; + bool found = access2.TryFindStoredValue(out string stored); + + await That(result).IsEqualTo("base"); + await That(found).IsTrue(); + await That(stored).IsEqualTo("base"); + } + + [Fact] + public async Task GetResult_WithBaseValue_4Param_StoresComputedValueForLaterLookup() + { + IndexerSetup setup = new( + new MockRegistry(MockBehavior.Default), + (IParameterMatch)It.IsAny(), + (IParameterMatch)It.IsAny(), + (IParameterMatch)It.IsAny(), + (IParameterMatch)It.IsAny()); + IndexerValueStorage storage = new(); + IndexerGetterAccess access1 = + new("p1", 1, "p2", 2, "p3", 3, "p4", 4) { Storage = storage, }; + + string result = setup.GetResult(access1, MockBehavior.Default, "base"); + + IndexerGetterAccess access2 = + new("p1", 1, "p2", 2, "p3", 3, "p4", 4) { Storage = storage, }; + bool found = access2.TryFindStoredValue(out string stored); + + await That(result).IsEqualTo("base"); + await That(found).IsTrue(); + await That(stored).IsEqualTo("base"); + } +} diff --git a/Tests/Mockolate.Internal.Tests/TestHelpers/FakeIndexerSetup.cs b/Tests/Mockolate.Internal.Tests/TestHelpers/FakeIndexerSetup.cs index a39eb31f..5a59439f 100644 --- a/Tests/Mockolate.Internal.Tests/TestHelpers/FakeIndexerSetup.cs +++ b/Tests/Mockolate.Internal.Tests/TestHelpers/FakeIndexerSetup.cs @@ -12,6 +12,9 @@ internal FakeIndexerSetup(bool match) : base(new MockRegistry(MockBehavior.Defau internal bool ShouldMatch { get; } + public static bool InvokeTryCast(object? value, out T result, MockBehavior behavior) + => TryCast(value, out result, behavior); + protected override bool MatchesAccess(IndexerAccess access) => ShouldMatch; public override bool? SkipBaseClass() => null; diff --git a/Tests/Mockolate.Tests/MockIndexers/SetupIndexerTests.TransitionToInParallelTests.cs b/Tests/Mockolate.Tests/MockIndexers/SetupIndexerTests.TransitionToInParallelTests.cs new file mode 100644 index 00000000..e8bed6b5 --- /dev/null +++ b/Tests/Mockolate.Tests/MockIndexers/SetupIndexerTests.TransitionToInParallelTests.cs @@ -0,0 +1,129 @@ +using Mockolate.Tests.TestHelpers; + +namespace Mockolate.Tests.MockIndexers; + +public sealed partial class SetupIndexerTests +{ + public sealed class TransitionToInParallelTests + { + [Fact] + public async Task OnGet_Arity1_ShouldTransitionEvenAfterPrecedingCallbackRan() + { + IScenarioService sut = IScenarioService.CreateMock(); + + sut.Mock.InScenario("a").Setup[It.IsAny()] + .OnGet.Do(() => { }) + .OnGet.TransitionTo("b"); + sut.Mock.TransitionTo("a"); + + _ = sut[1]; + + await That(((IMock)sut).MockRegistry.Scenario).IsEqualTo("b"); + } + + [Fact] + public async Task OnGet_Arity2_ShouldTransitionEvenAfterPrecedingCallbackRan() + { + IScenarioService sut = IScenarioService.CreateMock(); + + sut.Mock.InScenario("a").Setup[It.IsAny(), It.IsAny()] + .OnGet.Do(() => { }) + .OnGet.TransitionTo("b"); + sut.Mock.TransitionTo("a"); + + _ = sut[1, 2]; + + await That(((IMock)sut).MockRegistry.Scenario).IsEqualTo("b"); + } + + [Fact] + public async Task OnGet_Arity3_ShouldTransitionEvenAfterPrecedingCallbackRan() + { + IScenarioService sut = IScenarioService.CreateMock(); + + sut.Mock.InScenario("a").Setup[It.IsAny(), It.IsAny(), It.IsAny()] + .OnGet.Do(() => { }) + .OnGet.TransitionTo("b"); + sut.Mock.TransitionTo("a"); + + _ = sut[1, 2, 3]; + + await That(((IMock)sut).MockRegistry.Scenario).IsEqualTo("b"); + } + + [Fact] + public async Task OnGet_Arity4_ShouldTransitionEvenAfterPrecedingCallbackRan() + { + IScenarioService sut = IScenarioService.CreateMock(); + + sut.Mock.InScenario("a").Setup[It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()] + .OnGet.Do(() => { }) + .OnGet.TransitionTo("b"); + sut.Mock.TransitionTo("a"); + + _ = sut[1, 2, 3, 4]; + + await That(((IMock)sut).MockRegistry.Scenario).IsEqualTo("b"); + } + + [Fact] + public async Task OnSet_Arity1_ShouldTransitionEvenAfterPrecedingCallbackRan() + { + IScenarioService sut = IScenarioService.CreateMock(); + + sut.Mock.InScenario("a").Setup[It.IsAny()] + .OnSet.Do(() => { }) + .OnSet.TransitionTo("b"); + sut.Mock.TransitionTo("a"); + + sut[1] = 42; + + await That(((IMock)sut).MockRegistry.Scenario).IsEqualTo("b"); + } + + [Fact] + public async Task OnSet_Arity2_ShouldTransitionEvenAfterPrecedingCallbackRan() + { + IScenarioService sut = IScenarioService.CreateMock(); + + sut.Mock.InScenario("a").Setup[It.IsAny(), It.IsAny()] + .OnSet.Do(() => { }) + .OnSet.TransitionTo("b"); + sut.Mock.TransitionTo("a"); + + sut[1, 2] = 42; + + await That(((IMock)sut).MockRegistry.Scenario).IsEqualTo("b"); + } + + [Fact] + public async Task OnSet_Arity3_ShouldTransitionEvenAfterPrecedingCallbackRan() + { + IScenarioService sut = IScenarioService.CreateMock(); + + sut.Mock.InScenario("a").Setup[It.IsAny(), It.IsAny(), It.IsAny()] + .OnSet.Do(() => { }) + .OnSet.TransitionTo("b"); + sut.Mock.TransitionTo("a"); + + sut[1, 2, 3] = 42; + + await That(((IMock)sut).MockRegistry.Scenario).IsEqualTo("b"); + } + + [Fact] + public async Task OnSet_Arity4_ShouldTransitionEvenAfterPrecedingCallbackRan() + { + IScenarioService sut = IScenarioService.CreateMock(); + + sut.Mock.InScenario("a").Setup[It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()] + .OnSet.Do(() => { }) + .OnSet.TransitionTo("b"); + sut.Mock.TransitionTo("a"); + + sut[1, 2, 3, 4] = 42; + + await That(((IMock)sut).MockRegistry.Scenario).IsEqualTo("b"); + } + } +} From 3ffc01be4f6348139add73da199411e19ecb73ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Wed, 22 Apr 2026 20:51:34 +0200 Subject: [PATCH 03/13] refactor: drop dead matched out parameter from ExecuteReturnCallbacks The out bool matched was written (matched = false at entry, matched = true before returning a matched value) but every caller discarded it with out _, in both the hand-written IndexerSetup and the source generator template. Remove the parameter and the assignments - the method now just returns the (possibly overridden) value like the surrounding Execute*Callbacks helpers. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Sources/Sources.IndexerSetups.cs | 10 ++--- Source/Mockolate/Setup/IndexerSetup.cs | 40 ++++++++----------- 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/Source/Mockolate.SourceGenerators/Sources/Sources.IndexerSetups.cs b/Source/Mockolate.SourceGenerators/Sources/Sources.IndexerSetups.cs index 585576ab..197009bc 100644 --- a/Source/Mockolate.SourceGenerators/Sources/Sources.IndexerSetups.cs +++ b/Source/Mockolate.SourceGenerators/Sources/Sources.IndexerSetups.cs @@ -974,7 +974,7 @@ private static void AppendIndexerSetup(StringBuilder sb, int numberOfParameters) sb.AppendLine(); sb.Append("\t\t\tTValue currentValue = TryCast(baseValue, out TValue casted, behavior) ? casted : default!;").AppendLine(); sb.Append("\t\t\tcurrentValue = ExecuteGetterCallbacks(").Append(parameters).Append(", currentValue);").AppendLine(); - sb.Append("\t\t\tcurrentValue = ExecuteReturnCallbacks(").Append(parameters).Append(", currentValue, out _);").AppendLine(); + sb.Append("\t\t\tcurrentValue = ExecuteReturnCallbacks(").Append(parameters).Append(", currentValue);").AppendLine(); sb.Append("\t\t\taccess.StoreValue(currentValue);").AppendLine(); sb.Append("\t\t\treturn TryCast(currentValue, out TResult result, behavior) ? result : baseValue;").AppendLine(); sb.Append("\t\t}").AppendLine(); @@ -1010,7 +1010,7 @@ private static void AppendIndexerSetup(StringBuilder sb, int numberOfParameters) sb.Append("\t\t\t}").AppendLine(); sb.AppendLine(); sb.Append("\t\t\tcurrentValue = ExecuteGetterCallbacks(").Append(parameters).Append(", currentValue);").AppendLine(); - sb.Append("\t\t\tcurrentValue = ExecuteReturnCallbacks(").Append(parameters).Append(", currentValue, out _);").AppendLine(); + sb.Append("\t\t\tcurrentValue = ExecuteReturnCallbacks(").Append(parameters).Append(", currentValue);").AppendLine(); sb.Append("\t\t\taccess.StoreValue(currentValue);").AppendLine(); sb.Append("\t\t\treturn TryCast(currentValue, out TResult result, behavior) ? result : behavior.DefaultValue.Generate(default(TResult)!);").AppendLine(); sb.Append("\t\t}").AppendLine(); @@ -1046,7 +1046,7 @@ private static void AppendIndexerSetup(StringBuilder sb, int numberOfParameters) sb.Append("\t\t\t}").AppendLine(); sb.AppendLine(); sb.Append("\t\t\tcurrentValue = ExecuteGetterCallbacks(").Append(parameters).Append(", currentValue);").AppendLine(); - sb.Append("\t\t\tcurrentValue = ExecuteReturnCallbacks(").Append(parameters).Append(", currentValue, out _);").AppendLine(); + sb.Append("\t\t\tcurrentValue = ExecuteReturnCallbacks(").Append(parameters).Append(", currentValue);").AppendLine(); sb.Append("\t\t\taccess.StoreValue(currentValue);").AppendLine(); sb.Append("\t\t\treturn TryCast(currentValue, out TResult result, behavior) ? result : defaultValueGenerator();").AppendLine(); sb.Append("\t\t}").AppendLine(); @@ -1117,9 +1117,8 @@ private static void AppendIndexerSetup(StringBuilder sb, int numberOfParameters) // ExecuteReturnCallbacks (private) sb.Append("\t\tprivate TValue ExecuteReturnCallbacks(").Append( - string.Join(", ", Enumerable.Range(1, numberOfParameters).Select(i => $"T{i} p{i}"))).Append(", TValue currentValue, out bool matched)").AppendLine(); + string.Join(", ", Enumerable.Range(1, numberOfParameters).Select(i => $"T{i} p{i}"))).Append(", TValue currentValue)").AppendLine(); sb.Append("\t\t{").AppendLine(); - sb.Append("\t\t\tmatched = false;").AppendLine(); sb.Append("\t\t\tif (_returnCallbacks is not null)").AppendLine(); sb.Append("\t\t\t{").AppendLine(); sb.Append("\t\t\t\tforeach (Callback> _ in _returnCallbacks)").AppendLine(); @@ -1130,7 +1129,6 @@ private static void AppendIndexerSetup(StringBuilder sb, int numberOfParameters) sb.Append("\t\t\t\t\t\tstatic (count, @delegate, state) => @delegate(count, ").Append(stateParameters).Append(", state.currentValue),").AppendLine(); sb.Append("\t\t\t\t\t\tout TValue? newValue))").AppendLine(); sb.Append("\t\t\t\t\t{").AppendLine(); - sb.Append("\t\t\t\t\t\tmatched = true;").AppendLine(); sb.Append("\t\t\t\t\t\treturn newValue!;").AppendLine(); sb.Append("\t\t\t\t\t}").AppendLine(); sb.Append("\t\t\t\t}").AppendLine(); diff --git a/Source/Mockolate/Setup/IndexerSetup.cs b/Source/Mockolate/Setup/IndexerSetup.cs index b29d4d50..ac1fbeb7 100644 --- a/Source/Mockolate/Setup/IndexerSetup.cs +++ b/Source/Mockolate/Setup/IndexerSetup.cs @@ -560,7 +560,7 @@ public override TResult GetResult(IndexerAccess access, MockBehavior be TValue currentValue = TryCast(baseValue, out TValue casted, behavior) ? casted : default!; currentValue = ExecuteGetterCallbacks(p1, currentValue); - currentValue = ExecuteReturnCallbacks(p1, currentValue, out _); + currentValue = ExecuteReturnCallbacks(p1, currentValue); access.StoreValue(currentValue); return TryCast(currentValue, out TResult result, behavior) ? result : baseValue; } @@ -589,7 +589,7 @@ public override TResult GetResult(IndexerAccess access, MockBehavior be } currentValue = ExecuteGetterCallbacks(p1, currentValue); - currentValue = ExecuteReturnCallbacks(p1, currentValue, out _); + currentValue = ExecuteReturnCallbacks(p1, currentValue); access.StoreValue(currentValue); return TryCast(currentValue, out TResult result, behavior) ? result : behavior.DefaultValue.GenerateTyped(); @@ -619,7 +619,7 @@ public override TResult GetResult(IndexerAccess access, MockBehavior be } currentValue = ExecuteGetterCallbacks(p1, currentValue); - currentValue = ExecuteReturnCallbacks(p1, currentValue, out _); + currentValue = ExecuteReturnCallbacks(p1, currentValue); access.StoreValue(currentValue); return TryCast(currentValue, out TResult result, behavior) ? result : defaultValueGenerator(); } @@ -677,9 +677,8 @@ private TValue ExecuteGetterCallbacks(T1 p1, TValue currentValue) return currentValue; } - private TValue ExecuteReturnCallbacks(T1 p1, TValue currentValue, out bool matched) + private TValue ExecuteReturnCallbacks(T1 p1, TValue currentValue) { - matched = false; if (_returnCallbacks is not null) { foreach (Callback> _ in _returnCallbacks) @@ -690,7 +689,6 @@ private TValue ExecuteReturnCallbacks(T1 p1, TValue currentValue, out bool match static (count, @delegate, state) => @delegate(count, state.p1, state.currentValue), out TValue? newValue)) { - matched = true; return newValue!; } } @@ -1162,7 +1160,7 @@ public override TResult GetResult(IndexerAccess access, MockBehavior be TValue currentValue = TryCast(baseValue, out TValue casted, behavior) ? casted : default!; currentValue = ExecuteGetterCallbacks(p1, p2, currentValue); - currentValue = ExecuteReturnCallbacks(p1, p2, currentValue, out _); + currentValue = ExecuteReturnCallbacks(p1, p2, currentValue); access.StoreValue(currentValue); return TryCast(currentValue, out TResult result, behavior) ? result : baseValue; } @@ -1191,7 +1189,7 @@ public override TResult GetResult(IndexerAccess access, MockBehavior be } currentValue = ExecuteGetterCallbacks(p1, p2, currentValue); - currentValue = ExecuteReturnCallbacks(p1, p2, currentValue, out _); + currentValue = ExecuteReturnCallbacks(p1, p2, currentValue); access.StoreValue(currentValue); return TryCast(currentValue, out TResult result, behavior) ? result : behavior.DefaultValue.GenerateTyped(); @@ -1221,7 +1219,7 @@ public override TResult GetResult(IndexerAccess access, MockBehavior be } currentValue = ExecuteGetterCallbacks(p1, p2, currentValue); - currentValue = ExecuteReturnCallbacks(p1, p2, currentValue, out _); + currentValue = ExecuteReturnCallbacks(p1, p2, currentValue); access.StoreValue(currentValue); return TryCast(currentValue, out TResult result, behavior) ? result : defaultValueGenerator(); } @@ -1279,9 +1277,8 @@ private TValue ExecuteGetterCallbacks(T1 p1, T2 p2, TValue currentValue) return currentValue; } - private TValue ExecuteReturnCallbacks(T1 p1, T2 p2, TValue currentValue, out bool matched) + private TValue ExecuteReturnCallbacks(T1 p1, T2 p2, TValue currentValue) { - matched = false; if (_returnCallbacks is not null) { foreach (Callback> _ in _returnCallbacks) @@ -1292,7 +1289,6 @@ private TValue ExecuteReturnCallbacks(T1 p1, T2 p2, TValue currentValue, out boo static (count, @delegate, state) => @delegate(count, state.p1, state.p2, state.currentValue), out TValue? newValue)) { - matched = true; return newValue!; } } @@ -1781,7 +1777,7 @@ public override TResult GetResult(IndexerAccess access, MockBehavior be TValue currentValue = TryCast(baseValue, out TValue casted, behavior) ? casted : default!; currentValue = ExecuteGetterCallbacks(p1, p2, p3, currentValue); - currentValue = ExecuteReturnCallbacks(p1, p2, p3, currentValue, out _); + currentValue = ExecuteReturnCallbacks(p1, p2, p3, currentValue); access.StoreValue(currentValue); return TryCast(currentValue, out TResult result, behavior) ? result : baseValue; } @@ -1810,7 +1806,7 @@ public override TResult GetResult(IndexerAccess access, MockBehavior be } currentValue = ExecuteGetterCallbacks(p1, p2, p3, currentValue); - currentValue = ExecuteReturnCallbacks(p1, p2, p3, currentValue, out _); + currentValue = ExecuteReturnCallbacks(p1, p2, p3, currentValue); access.StoreValue(currentValue); return TryCast(currentValue, out TResult result, behavior) ? result : behavior.DefaultValue.GenerateTyped(); @@ -1840,7 +1836,7 @@ public override TResult GetResult(IndexerAccess access, MockBehavior be } currentValue = ExecuteGetterCallbacks(p1, p2, p3, currentValue); - currentValue = ExecuteReturnCallbacks(p1, p2, p3, currentValue, out _); + currentValue = ExecuteReturnCallbacks(p1, p2, p3, currentValue); access.StoreValue(currentValue); return TryCast(currentValue, out TResult result, behavior) ? result : defaultValueGenerator(); } @@ -1898,9 +1894,8 @@ private TValue ExecuteGetterCallbacks(T1 p1, T2 p2, T3 p3, TValue currentValue) return currentValue; } - private TValue ExecuteReturnCallbacks(T1 p1, T2 p2, T3 p3, TValue currentValue, out bool matched) + private TValue ExecuteReturnCallbacks(T1 p1, T2 p2, T3 p3, TValue currentValue) { - matched = false; if (_returnCallbacks is not null) { foreach (Callback> _ in _returnCallbacks) @@ -1911,7 +1906,6 @@ private TValue ExecuteReturnCallbacks(T1 p1, T2 p2, T3 p3, TValue currentValue, static (count, @delegate, state) => @delegate(count, state.p1, state.p2, state.p3, state.currentValue), out TValue? newValue)) { - matched = true; return newValue!; } } @@ -2409,7 +2403,7 @@ public override TResult GetResult(IndexerAccess access, MockBehavior be TValue currentValue = TryCast(baseValue, out TValue casted, behavior) ? casted : default!; currentValue = ExecuteGetterCallbacks(p1, p2, p3, p4, currentValue); - currentValue = ExecuteReturnCallbacks(p1, p2, p3, p4, currentValue, out _); + currentValue = ExecuteReturnCallbacks(p1, p2, p3, p4, currentValue); access.StoreValue(currentValue); return TryCast(currentValue, out TResult result, behavior) ? result : baseValue; } @@ -2438,7 +2432,7 @@ public override TResult GetResult(IndexerAccess access, MockBehavior be } currentValue = ExecuteGetterCallbacks(p1, p2, p3, p4, currentValue); - currentValue = ExecuteReturnCallbacks(p1, p2, p3, p4, currentValue, out _); + currentValue = ExecuteReturnCallbacks(p1, p2, p3, p4, currentValue); access.StoreValue(currentValue); return TryCast(currentValue, out TResult result, behavior) ? result : behavior.DefaultValue.GenerateTyped(); @@ -2468,7 +2462,7 @@ public override TResult GetResult(IndexerAccess access, MockBehavior be } currentValue = ExecuteGetterCallbacks(p1, p2, p3, p4, currentValue); - currentValue = ExecuteReturnCallbacks(p1, p2, p3, p4, currentValue, out _); + currentValue = ExecuteReturnCallbacks(p1, p2, p3, p4, currentValue); access.StoreValue(currentValue); return TryCast(currentValue, out TResult result, behavior) ? result : defaultValueGenerator(); } @@ -2526,9 +2520,8 @@ private TValue ExecuteGetterCallbacks(T1 p1, T2 p2, T3 p3, T4 p4, TValue current return currentValue; } - private TValue ExecuteReturnCallbacks(T1 p1, T2 p2, T3 p3, T4 p4, TValue currentValue, out bool matched) + private TValue ExecuteReturnCallbacks(T1 p1, T2 p2, T3 p3, T4 p4, TValue currentValue) { - matched = false; if (_returnCallbacks is not null) { foreach (Callback> _ in _returnCallbacks) @@ -2539,7 +2532,6 @@ private TValue ExecuteReturnCallbacks(T1 p1, T2 p2, T3 p3, T4 p4, TValue current static (count, @delegate, state) => @delegate(count, state.p1, state.p2, state.p3, state.p4, state.currentValue), out TValue? newValue)) { - matched = true; return newValue!; } } From 46a163cd62e8fd24af3d15edbda0c6929c3d71ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Wed, 22 Apr 2026 21:01:38 +0200 Subject: [PATCH 04/13] test: kill InParallel mutants in method/event/property TransitionTo Covers the same InParallel-must-fire-after-preceding-callback gap as the indexer group for the other fluent setups: void methods (arity 0-4), return methods (arity 1-4), event subscribe/unsubscribe, and property getter/setter. Setting up Do + TransitionTo on the same sequence makes the transition callback observable only when RunInParallel is set. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ScenarioTransitionToInParallelTests.cs | 204 ++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 Tests/Mockolate.Tests/ScenarioTransitionToInParallelTests.cs diff --git a/Tests/Mockolate.Tests/ScenarioTransitionToInParallelTests.cs b/Tests/Mockolate.Tests/ScenarioTransitionToInParallelTests.cs new file mode 100644 index 00000000..2445111c --- /dev/null +++ b/Tests/Mockolate.Tests/ScenarioTransitionToInParallelTests.cs @@ -0,0 +1,204 @@ +using Mockolate.Tests.TestHelpers; + +namespace Mockolate.Tests; + +public sealed class ScenarioTransitionToInParallelTests +{ + [Fact] + public async Task VoidMethod_Arity0_TransitionsEvenAfterPrecedingCallbackRan() + { + IScenarioService sut = IScenarioService.CreateMock(); + + sut.Mock.InScenario("a").Setup.VoidMethod0() + .Do(() => { }) + .TransitionTo("b"); + sut.Mock.TransitionTo("a"); + + sut.VoidMethod0(); + + await That(((IMock)sut).MockRegistry.Scenario).IsEqualTo("b"); + } + + [Fact] + public async Task VoidMethod_Arity1_TransitionsEvenAfterPrecedingCallbackRan() + { + IScenarioService sut = IScenarioService.CreateMock(); + + sut.Mock.InScenario("a").Setup.VoidMethod1(It.IsAny()) + .Do(() => { }) + .TransitionTo("b"); + sut.Mock.TransitionTo("a"); + + sut.VoidMethod1(42); + + await That(((IMock)sut).MockRegistry.Scenario).IsEqualTo("b"); + } + + [Fact] + public async Task VoidMethod_Arity2_TransitionsEvenAfterPrecedingCallbackRan() + { + IScenarioService sut = IScenarioService.CreateMock(); + + sut.Mock.InScenario("a").Setup.VoidMethod2(It.IsAny(), It.IsAny()) + .Do(() => { }) + .TransitionTo("b"); + sut.Mock.TransitionTo("a"); + + sut.VoidMethod2(1, 2); + + await That(((IMock)sut).MockRegistry.Scenario).IsEqualTo("b"); + } + + [Fact] + public async Task VoidMethod_Arity3_TransitionsEvenAfterPrecedingCallbackRan() + { + IScenarioService sut = IScenarioService.CreateMock(); + + sut.Mock.InScenario("a").Setup.VoidMethod3(It.IsAny(), It.IsAny(), It.IsAny()) + .Do(() => { }) + .TransitionTo("b"); + sut.Mock.TransitionTo("a"); + + sut.VoidMethod3(1, 2, 3); + + await That(((IMock)sut).MockRegistry.Scenario).IsEqualTo("b"); + } + + [Fact] + public async Task VoidMethod_Arity4_TransitionsEvenAfterPrecedingCallbackRan() + { + IScenarioService sut = IScenarioService.CreateMock(); + + sut.Mock.InScenario("a").Setup + .VoidMethod4(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()) + .Do(() => { }) + .TransitionTo("b"); + sut.Mock.TransitionTo("a"); + + sut.VoidMethod4(1, 2, 3, 4); + + await That(((IMock)sut).MockRegistry.Scenario).IsEqualTo("b"); + } + + [Fact] + public async Task ReturnMethod_Arity1_TransitionsEvenAfterPrecedingCallbackRan() + { + IScenarioService sut = IScenarioService.CreateMock(); + + sut.Mock.InScenario("a").Setup.ReturnMethod1(It.IsAny()) + .Do(() => { }) + .TransitionTo("b"); + sut.Mock.TransitionTo("a"); + + _ = sut.ReturnMethod1(42); + + await That(((IMock)sut).MockRegistry.Scenario).IsEqualTo("b"); + } + + [Fact] + public async Task ReturnMethod_Arity2_TransitionsEvenAfterPrecedingCallbackRan() + { + IScenarioService sut = IScenarioService.CreateMock(); + + sut.Mock.InScenario("a").Setup.ReturnMethod2(It.IsAny(), It.IsAny()) + .Do(() => { }) + .TransitionTo("b"); + sut.Mock.TransitionTo("a"); + + _ = sut.ReturnMethod2(1, 2); + + await That(((IMock)sut).MockRegistry.Scenario).IsEqualTo("b"); + } + + [Fact] + public async Task ReturnMethod_Arity3_TransitionsEvenAfterPrecedingCallbackRan() + { + IScenarioService sut = IScenarioService.CreateMock(); + + sut.Mock.InScenario("a").Setup + .ReturnMethod3(It.IsAny(), It.IsAny(), It.IsAny()) + .Do(() => { }) + .TransitionTo("b"); + sut.Mock.TransitionTo("a"); + + _ = sut.ReturnMethod3(1, 2, 3); + + await That(((IMock)sut).MockRegistry.Scenario).IsEqualTo("b"); + } + + [Fact] + public async Task ReturnMethod_Arity4_TransitionsEvenAfterPrecedingCallbackRan() + { + IScenarioService sut = IScenarioService.CreateMock(); + + sut.Mock.InScenario("a").Setup + .ReturnMethod4(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()) + .Do(() => { }) + .TransitionTo("b"); + sut.Mock.TransitionTo("a"); + + _ = sut.ReturnMethod4(1, 2, 3, 4); + + await That(((IMock)sut).MockRegistry.Scenario).IsEqualTo("b"); + } + + [Fact] + public async Task EventSubscription_TransitionsEvenAfterPrecedingCallbackRan() + { + IScenarioService sut = IScenarioService.CreateMock(); + + sut.Mock.InScenario("a").Setup.Event.OnSubscribed + .Do(() => { }) + .OnSubscribed.TransitionTo("b"); + sut.Mock.TransitionTo("a"); + + sut.Event += (_, _) => { }; + + await That(((IMock)sut).MockRegistry.Scenario).IsEqualTo("b"); + } + + [Fact] + public async Task EventUnsubscription_TransitionsEvenAfterPrecedingCallbackRan() + { + IScenarioService sut = IScenarioService.CreateMock(); + + sut.Mock.InScenario("a").Setup.Event.OnUnsubscribed + .Do(() => { }) + .OnUnsubscribed.TransitionTo("b"); + sut.Mock.TransitionTo("a"); + + sut.Event -= (_, _) => { }; + + await That(((IMock)sut).MockRegistry.Scenario).IsEqualTo("b"); + } + + [Fact] + public async Task PropertyGetter_TransitionsEvenAfterPrecedingCallbackRan() + { + IScenarioService sut = IScenarioService.CreateMock(); + + sut.Mock.InScenario("a").Setup.Property.OnGet + .Do(() => { }) + .OnGet.TransitionTo("b"); + sut.Mock.TransitionTo("a"); + + _ = sut.Property; + + await That(((IMock)sut).MockRegistry.Scenario).IsEqualTo("b"); + } + + [Fact] + public async Task PropertySetter_TransitionsEvenAfterPrecedingCallbackRan() + { + IScenarioService sut = IScenarioService.CreateMock(); + + sut.Mock.InScenario("a").Setup.Property.OnSet + .Do(() => { }) + .TransitionTo("b"); + sut.Mock.TransitionTo("a"); + + sut.Property = 42; + + await That(((IMock)sut).MockRegistry.Scenario).IsEqualTo("b"); + } +} From 820c2007022b3875151c5e407c5595f2d3f11322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Wed, 22 Apr 2026 21:11:02 +0200 Subject: [PATCH 05/13] test: kill MethodSetups find, PropertySetups count, EventSetup For, and Callback state-passing mutants - MockSetups.MethodSetups.GetLatestOrDefault: add a single-setup test so index 0 is exercised (i >= 0 vs i > 0). - MockSetups.PropertySetups.Add: verify that replacing a Default with a user setup increments Count by exactly one, pinning the wasDefault && !isDefault gate and the _count + 1 write. - EventSetup OnSubscribed/OnUnsubscribed: pair a .For(2) callback with a trailing .Do and subscribe 4 times; without .For(2) the counts split evenly instead of 3/1. - Callback state-passing Invoke and Invoke: mirror the existing non-state-passing tests so the separate matchingCount/forIterationCount and count-1 bookkeeping is observed. Also re-adds the lost .OnSet. prefix on PropertySetter_TransitionsEven test that an auto-formatter had stripped. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../SetupMutationTests.cs | 129 ++++++++++++++++++ .../SetupEventTests.ForMutationTests.cs | 50 +++++++ .../ScenarioTransitionToInParallelTests.cs | 2 +- 3 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 Tests/Mockolate.Internal.Tests/SetupMutationTests.cs create mode 100644 Tests/Mockolate.Tests/MockEvents/SetupEventTests.ForMutationTests.cs diff --git a/Tests/Mockolate.Internal.Tests/SetupMutationTests.cs b/Tests/Mockolate.Internal.Tests/SetupMutationTests.cs new file mode 100644 index 00000000..068af090 --- /dev/null +++ b/Tests/Mockolate.Internal.Tests/SetupMutationTests.cs @@ -0,0 +1,129 @@ +using System.Collections.Generic; +using Mockolate.Internal.Tests.TestHelpers; +using Mockolate.Setup; + +namespace Mockolate.Internal.Tests; + +public class SetupMutationTests +{ + public sealed class MethodSetupsFindTests + { + [Fact] + public async Task GetLatestOrDefault_WithSingleMatchingSetup_ShouldReturnIt() + { + MockSetups.MethodSetups setups = new(); + FakeMethodSetup setup = new(); + setups.Add(setup); + + MethodSetup? result = setups.GetLatestOrDefault(_ => true); + + await That(result).IsSameAs(setup); + } + } + + public sealed class PropertySetupsCountTests + { + [Fact] + public async Task Add_ReplacingDefaultWithUserSetup_ShouldIncrementCountByOne() + { + MockSetups.PropertySetups setups = new(); + PropertySetup defaultSetup = new PropertySetup.Default("p", 0); + FakePropertySetup userSetup = new("p"); + + setups.Add(defaultSetup); + await That(setups.Count).IsEqualTo(0); + + setups.Add(userSetup); + + await That(setups.Count).IsEqualTo(1); + setups.TryGetValue("p", out PropertySetup? found); + await That(found).IsSameAs(userSetup); + } + } + + public sealed class StatePassingInvokeTests + { + [Fact] + public async Task ShouldIncludeIndexWhenMatching() + { + bool wasInvoked = false; + List values = []; + Callback sut = new(() => { }); + sut.Only(2); + sut.When(v => v > 1); + + int index = 0; + for (int i = 0; i < 5; i++) + { + sut.Invoke(wasInvoked, ref index, values, static (v, _, list) => list.Add(v)); + } + + await That(values).IsEqualTo([2, 3,]); + } + + [Fact] + public async Task ShouldLimitExecutionWhenRunningInParallel() + { + bool wasInvoked = false; + List values = []; + Callback sut = new(() => { }); + sut.Only(2); + sut.InParallel(); + + int index = 0; + for (int i = 0; i < 5; i++) + { + sut.Invoke(wasInvoked, ref index, values, static (v, _, list) => list.Add(v)); + } + + await That(values).IsEqualTo([0, 1,]); + } + + [Theory] + [InlineData(2, 2, 0, 1, 1, 2, 2, 2, 2, 2, 2, 2)] + [InlineData(2, 3, 0, 1, 1, 2, 2, 3, 3, 3, 3, 3)] + public async Task ShouldIncrementIndexWheneverForIsExhausted( + int @for, int only, params int[] expectResult) + { + bool wasInvoked = false; + List indexValues = []; + Callback sut = new(() => { }); + sut.For(@for); + sut.Only(only); + + int index = 0; + for (int iteration = 1; iteration <= 10; iteration++) + { + sut.Invoke(wasInvoked, ref index, 0, static (_, _, _) => { }); + indexValues.Add(index); + } + + await That(indexValues).IsEqualTo(expectResult); + } + } + + public sealed class StatePassingReturnInvokeTests + { + [Fact] + public async Task ShouldIncludeIndexWhenMatching() + { + List values = []; + Callback sut = new(() => { }); + sut.Only(2); + sut.When(v => v > 1); + + int index = 0; + for (int i = 0; i < 5; i++) + { + sut.Invoke, string>(ref index, values, + static (v, _, list) => + { + list.Add(v); + return ""; + }, out _); + } + + await That(values).IsEqualTo([2, 3,]); + } + } +} diff --git a/Tests/Mockolate.Tests/MockEvents/SetupEventTests.ForMutationTests.cs b/Tests/Mockolate.Tests/MockEvents/SetupEventTests.ForMutationTests.cs new file mode 100644 index 00000000..e3b38abb --- /dev/null +++ b/Tests/Mockolate.Tests/MockEvents/SetupEventTests.ForMutationTests.cs @@ -0,0 +1,50 @@ +using Mockolate.Tests.TestHelpers; + +namespace Mockolate.Tests.MockEvents; + +public sealed class SetupEventForMutationTests +{ + [Fact] + public async Task OnSubscribed_For2_HoldsFirstCallbackForTwoSubscriptionsBeforeAdvancing() + { + int count1 = 0; + int count2 = 0; + IChocolateDispenser sut = IChocolateDispenser.CreateMock(); + + sut.Mock.Setup.ChocolateDispensed + .OnSubscribed.Do(() => { count1++; }).For(2) + .OnSubscribed.Do(() => { count2++; }); + + sut.ChocolateDispensed += Handler; + sut.ChocolateDispensed += Handler; + sut.ChocolateDispensed += Handler; + sut.ChocolateDispensed += Handler; + + await That(count1).IsEqualTo(3); + await That(count2).IsEqualTo(1); + + void Handler(string type, int amount) { } + } + + [Fact] + public async Task OnUnsubscribed_For2_HoldsFirstCallbackForTwoUnsubscriptionsBeforeAdvancing() + { + int count1 = 0; + int count2 = 0; + IChocolateDispenser sut = IChocolateDispenser.CreateMock(); + + sut.Mock.Setup.ChocolateDispensed + .OnUnsubscribed.Do(() => { count1++; }).For(2) + .OnUnsubscribed.Do(() => { count2++; }); + + sut.ChocolateDispensed -= Handler; + sut.ChocolateDispensed -= Handler; + sut.ChocolateDispensed -= Handler; + sut.ChocolateDispensed -= Handler; + + await That(count1).IsEqualTo(3); + await That(count2).IsEqualTo(1); + + void Handler(string type, int amount) { } + } +} diff --git a/Tests/Mockolate.Tests/ScenarioTransitionToInParallelTests.cs b/Tests/Mockolate.Tests/ScenarioTransitionToInParallelTests.cs index 2445111c..e15e5c53 100644 --- a/Tests/Mockolate.Tests/ScenarioTransitionToInParallelTests.cs +++ b/Tests/Mockolate.Tests/ScenarioTransitionToInParallelTests.cs @@ -194,7 +194,7 @@ public async Task PropertySetter_TransitionsEvenAfterPrecedingCallbackRan() sut.Mock.InScenario("a").Setup.Property.OnSet .Do(() => { }) - .TransitionTo("b"); + .OnSet.TransitionTo("b"); sut.Mock.TransitionTo("a"); sut.Property = 42; From 8de011a2eebd8f6abd9779a74b768de1ef972ebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Wed, 22 Apr 2026 21:14:34 +0200 Subject: [PATCH 06/13] test: kill PropertySetup InitializeWith and Default typeof-branch mutants - Repeated auto-init via IInteractivePropertySetup.InitializeWith must be idempotent, pinning the _isUserInitialized || _isInitialized guard. - Repeated user-facing InitializeWith must also be a no-op after the first call, pinning the _isUserInitialized = true write. - Default.InvokeGetter with mismatched TResult must fall through to the defaultValueGenerator and never take the Unsafe.As reinterpret path - covered with an int backing value whose bit pattern is a valid float, so a wrong branch would reinterpret 3.0f. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../SetupMutationTests.cs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/Tests/Mockolate.Internal.Tests/SetupMutationTests.cs b/Tests/Mockolate.Internal.Tests/SetupMutationTests.cs index 068af090..46fd8a49 100644 --- a/Tests/Mockolate.Internal.Tests/SetupMutationTests.cs +++ b/Tests/Mockolate.Internal.Tests/SetupMutationTests.cs @@ -102,6 +102,48 @@ public async Task ShouldIncrementIndexWheneverForIsExhausted( } } + public sealed class PropertySetupInitializeTests + { + [Fact] + public async Task AutoInitializeWith_WhenAlreadyInitialized_ShouldNotOverwriteValue() + { + FakePropertySetup setup = new("p"); + IInteractivePropertySetup interactive = setup; + + interactive.InitializeWith(5); + interactive.InitializeWith(10); + + int value = interactive.InvokeGetter(null, MockBehavior.Default, () => -1); + await That(value).IsEqualTo(5); + } + + [Fact] + public async Task UserInitializeWith_SecondCall_ShouldNotOverwriteValue() + { + FakePropertySetup setup = new("p"); + IPropertySetup userFacing = setup; + + userFacing.InitializeWith(5); + userFacing.InitializeWith(10); + + int value = ((IInteractivePropertySetup)setup).InvokeGetter(null, MockBehavior.Default, () => -1); + await That(value).IsEqualTo(5); + } + + [Fact] + public async Task DefaultInvokeGetter_WhenRequestedTypeDiffersFromBackingType_ShouldFallBackToGenerator() + { + // 0x40400000 reinterpreted via Unsafe.As would yield 3.0f; the correct path + // must take the typeof-equality branch and fall through to the defaultValueGenerator. + PropertySetup.Default setup = new("p", 0x40400000); + IInteractivePropertySetup interactive = setup; + + float value = interactive.InvokeGetter(null, MockBehavior.Default, () => 99f); + + await That(value).IsEqualTo(99f); + } + } + public sealed class StatePassingReturnInvokeTests { [Fact] From 116cc7f40e5fc2532cb73506af3150e07eccf233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Wed, 22 Apr 2026 21:20:59 +0200 Subject: [PATCH 07/13] refactor: eliminate or mark remaining IndexerSetup-adjacent dead mutants - MockSetups.Properties.Add: replace the manual break loop with Array.FindIndex. Unique-name invariant makes the break removal un-observable, so folding the search into the framework method both eliminates the mutant and simplifies the code. - PropertySetup: Stryker-disable three mutants that are equivalent to the original by construction - Default.IsValueInitialized (its InitializeValue is a no-op), the Unsafe.As fast path in Default.InvokeGetter (the boxing fallback produces the same value), and the _isInitialized = true write in user InitializeWith (shadowed by the _isUserInitialized guard that follows). Each site carries a reason explaining why flipping the mutant changes nothing observable. Co-Authored-By: Claude Opus 4.7 (1M context) --- Source/Mockolate/Setup/MockSetups.Properties.cs | 11 ++--------- Source/Mockolate/Setup/PropertySetup.cs | 3 +++ 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/Source/Mockolate/Setup/MockSetups.Properties.cs b/Source/Mockolate/Setup/MockSetups.Properties.cs index 51eee97c..ed3c3e81 100644 --- a/Source/Mockolate/Setup/MockSetups.Properties.cs +++ b/Source/Mockolate/Setup/MockSetups.Properties.cs @@ -32,15 +32,8 @@ public void Add(PropertySetup setup) lock (_writeLock) { PropertySetup[] old = _storage ?? []; - int existingIndex = -1; - for (int i = 0; i < old.Length; i++) - { - if (string.Equals(old[i].Name, setup.Name, StringComparison.Ordinal)) - { - existingIndex = i; - break; - } - } + int existingIndex = Array.FindIndex(old, + s => string.Equals(s.Name, setup.Name, StringComparison.Ordinal)); if (existingIndex >= 0) { diff --git a/Source/Mockolate/Setup/PropertySetup.cs b/Source/Mockolate/Setup/PropertySetup.cs index fd56c36a..aae6f7c7 100644 --- a/Source/Mockolate/Setup/PropertySetup.cs +++ b/Source/Mockolate/Setup/PropertySetup.cs @@ -96,6 +96,7 @@ internal abstract class Default(string name) : PropertySetup public override string Name => name; /// + // Stryker disable once Boolean : Default.InitializeValue is a no-op, so flipping this to false only triggers a redundant re-init call with the same observable outcome. internal override bool IsValueInitialized => true; /// @@ -144,6 +145,7 @@ protected override TResult InvokeGetter(MockBehavior behavior, Func(ref _value); } @@ -436,6 +438,7 @@ public IPropertySetup InitializeWith(T value) } _value = value; + // Stryker disable once Boolean : flipping this to false only leaves IsValueInitialized wrong for one redundant auto-init cycle that is no-op'd by the _isUserInitialized guard on the next line. _isInitialized = true; _isUserInitialized = true; } From c905267fb4a39024282234a1381107f879443b1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Wed, 22 Apr 2026 21:33:22 +0200 Subject: [PATCH 08/13] test: kill MockRegistry, MockBehavior, MethodInvocation, and event-scope mutants - MockRegistry.InitializeStorage: exercise the boundary value (0 must not throw) and the rejected value (-1 throws with a descriptive message). - MockRegistry wrap constructor: verify SkipInteractionRecording is copied onto the wrapped MockInteractions instance, pinning the object-initializer. - MockRegistry.GetIndexerSetup(Func): with an active scenario, scoped setups must take precedence over the global setup, pinning the IsNullOrEmpty(Scenario) guard's negation. - GetIndexerFallback / ApplyIndexerGetter: verify computed values are cached on the access for later lookups, so removing the StoreValue calls is observable across repeated reads. - GetEventSetupsByName scenario scoping: scoped event setups must hide the global setups, and an empty scoped bucket must still fall through to the global setups - together pinning the hasScoped bookkeeping, the if/yield-break gate, and the yield-break statement. - MockBehavior.ToString: multiple flags must all appear in the output, pinning the parts ??= [] coalesce-assignment. - MethodInvocation.ToString: both the value path and the null fallback path for the third parameter must render, pinning the Parameter3?.ToString() ?? "null" fragment. - Stryker-disable five mutants that are equivalent to their original by construction (two String variants of IsNullOrEmpty, the forceReinit flag on the interface-only property flow, the _verified HashSet initializer, and the Clear block that nulls _verified). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Interactions/MockInteractions.cs | 2 + Source/Mockolate/MockRegistry.Interactions.cs | 2 + .../MockRegistryMutationTests.cs | 162 ++++++++++++++++++ .../SetupEventTests.ScenarioScopingTests.cs | 57 ++++++ 4 files changed, 223 insertions(+) create mode 100644 Tests/Mockolate.Internal.Tests/MockRegistryMutationTests.cs create mode 100644 Tests/Mockolate.Tests/MockEvents/SetupEventTests.ScenarioScopingTests.cs diff --git a/Source/Mockolate/Interactions/MockInteractions.cs b/Source/Mockolate/Interactions/MockInteractions.cs index eec37d68..88df6615 100644 --- a/Source/Mockolate/Interactions/MockInteractions.cs +++ b/Source/Mockolate/Interactions/MockInteractions.cs @@ -115,6 +115,7 @@ internal void Verified(IEnumerable interactions) { lock (_verifiedLock) { + // Stryker disable once Collection : seeding _verified with { default } adds a null entry that HashSet.Contains never matches against a real interaction, leaving GetUnverifiedInteractions behavior identical. _verified ??= []; foreach (IInteraction interaction in interactions) { @@ -130,6 +131,7 @@ internal void Clear() _interactions.Clear(); } + // Stryker disable once Block : keeping the stale _verified set after Clear never causes an unverified interaction to appear verified because Clear also empties _interactions and any later-added interaction is a fresh reference not present in the stale set. lock (_verifiedLock) { _verified = null; diff --git a/Source/Mockolate/MockRegistry.Interactions.cs b/Source/Mockolate/MockRegistry.Interactions.cs index 44e3ef1d..aeb4fb36 100644 --- a/Source/Mockolate/MockRegistry.Interactions.cs +++ b/Source/Mockolate/MockRegistry.Interactions.cs @@ -110,6 +110,7 @@ public IEnumerable GetMethodSetups(string methodName) where T : MethodSetu /// The matching setup, or when none was found. public T? GetIndexerSetup(Func predicate) where T : IndexerSetup { + // Stryker disable once String : TryGetScenario returns the root bucket when given a null or empty name, so replacing the IsNullOrEmpty guard with a null/empty comparison just re-searches the same global Indexers and produces the same result. if (!string.IsNullOrEmpty(Scenario) && Setup.TryGetScenario(Scenario, out MockScenarioSetup? scopedBucket)) { @@ -361,6 +362,7 @@ public TResult GetProperty(string propertyName, Func defaultVa PropertySetup matchingSetup; if (baseValueAccessor is null) { + // Stryker disable once Boolean : forceReinit only matters when a base class accessor exists; on this branch there is none, so flipping it to true triggers an extra InitializeWith call that is gated by _isUserInitialized || _isInitialized and becomes a no-op. matchingSetup = ResolvePropertySetup(propertyName, defaultValueGenerator, null, false); return ((IInteractivePropertySetup)matchingSetup).InvokeGetter(interaction, Behavior, diff --git a/Tests/Mockolate.Internal.Tests/MockRegistryMutationTests.cs b/Tests/Mockolate.Internal.Tests/MockRegistryMutationTests.cs new file mode 100644 index 00000000..ff4ad0b7 --- /dev/null +++ b/Tests/Mockolate.Internal.Tests/MockRegistryMutationTests.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using Mockolate.Interactions; +using Mockolate.Internal.Tests.TestHelpers; +using Mockolate.Setup; + +namespace Mockolate.Internal.Tests; + +public sealed class MockRegistryMutationTests +{ + public sealed class GetIndexerSetupScenarioScopingTests + { + [Fact] + public async Task WithActiveScenario_ShouldReturnScopedSetupOverGlobalSetup() + { + MockRegistry registry = new(MockBehavior.Default); + FakeIndexerSetup globalSetup = new(match: true); + FakeIndexerSetup scopedSetup = new(match: true); + registry.Setup.Indexers.Add(globalSetup); + registry.Setup.GetOrCreateScenario("a").Indexers.Add(scopedSetup); + + registry.TransitionTo("a"); + IndexerSetup? result = registry.GetIndexerSetup(_ => true); + + await That(result).IsSameAs(scopedSetup); + } + + [Fact] + public async Task WithoutActiveScenario_ShouldFallBackToGlobalSetup() + { + MockRegistry registry = new(MockBehavior.Default); + FakeIndexerSetup globalSetup = new(match: true); + FakeIndexerSetup scopedSetup = new(match: true); + registry.Setup.Indexers.Add(globalSetup); + registry.Setup.GetOrCreateScenario("a").Indexers.Add(scopedSetup); + + IndexerSetup? result = registry.GetIndexerSetup(_ => true); + + await That(result).IsSameAs(globalSetup); + } + } + + + public sealed class IndexerFallbackStoresValueTests + { + [Fact] + public async Task GetIndexerFallback_ShouldStoreDefaultForLaterLookup() + { + int counter = 0; + MockBehavior behavior = MockBehavior.Default.WithDefaultValueFor(() => ++counter); + MockRegistry registry = new(behavior); + IndexerGetterAccess access1 = new("p", 1); + IndexerGetterAccess access2 = new("p", 1); + + int first = registry.GetIndexerFallback(access1, 0); + int second = registry.GetIndexerFallback(access2, 0); + + await That(first).IsEqualTo(1); + await That(second).IsEqualTo(1); + } + + [Fact] + public async Task ApplyIndexerGetter_WithNullSetup_ShouldStoreBaseValueForLaterLookup() + { + MockRegistry registry = new(MockBehavior.Default); + IndexerGetterAccess access1 = new("p", 1); + IndexerGetterAccess access2 = new("p", 1); + + int first = registry.ApplyIndexerGetter(access1, null, 42, 0); + int second = registry.ApplyIndexerGetter(access2, null, 99, 0); + + await That(first).IsEqualTo(42); + await That(second).IsEqualTo(42); + } + } + + public sealed class InitializeStorageTests + { + [Fact] + public async Task WithZero_ShouldNotThrow() + { + MockRegistry registry = new(MockBehavior.Default); + + void Act() + => registry.InitializeStorage(0); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WithNegative_ShouldThrowWithDescriptiveMessage() + { + MockRegistry registry = new(MockBehavior.Default); + + void Act() + => registry.InitializeStorage(-1); + + await That(Act).Throws() + .WithMessage("*non-negative*").AsWildcard(); + } + } + + public sealed class WrapConstructorTests + { + [Fact] + public async Task WhenBehaviorSkipsInteractionRecording_WrappedRegistryAlsoSkipsRecording() + { + MockBehavior behavior = MockBehavior.Default.SkippingInteractionRecording(); + MockRegistry original = new(behavior); + object wrapped = new(); + + MockRegistry wrappingRegistry = new(original, wrapped); + + MethodInvocation interaction = new("test"); + ((IMockInteractions)wrappingRegistry.Interactions).RegisterInteraction(interaction); + + await That(wrappingRegistry.Interactions.Count).IsEqualTo(0); + } + } + + public sealed class MethodInvocationThreeParameterToStringTests + { + [Fact] + public async Task Should_Include_ThirdParameterValue() + { + MethodInvocation sut = new("MyType.MyMethod", "a", 1, "b", 2, "c", 3); + + string result = sut.ToString(); + + await That(result).IsEqualTo("invoke method MyMethod(1, 2, 3)"); + } + + [Fact] + public async Task WithNullThirdParameter_Should_FormatAsNullLiteral() + { + MethodInvocation sut = + new("MyType.MyMethod", "a", null, "b", null, "c", null); + + string result = sut.ToString(); + + await That(result).IsEqualTo("invoke method MyMethod(null, null, null)"); + } + } + + public sealed class MockBehaviorToStringTests + { + [Fact] + public async Task WithMultipleFlags_ShouldKeepAllPartsInOutput() + { + MockBehavior behavior = MockBehavior.Default + .ThrowingWhenNotSetup() + .SkippingBaseClass() + .SkippingInteractionRecording(); + + string result = behavior.ToString(); + + await That(result).Contains("ThrowingWhenNotSetup"); + await That(result).Contains("SkippingBaseClass"); + await That(result).Contains("SkippingInteractionRecording"); + } + } +} diff --git a/Tests/Mockolate.Tests/MockEvents/SetupEventTests.ScenarioScopingTests.cs b/Tests/Mockolate.Tests/MockEvents/SetupEventTests.ScenarioScopingTests.cs new file mode 100644 index 00000000..5d07b87d --- /dev/null +++ b/Tests/Mockolate.Tests/MockEvents/SetupEventTests.ScenarioScopingTests.cs @@ -0,0 +1,57 @@ +using Mockolate.Tests.TestHelpers; + +namespace Mockolate.Tests.MockEvents; + +public sealed class SetupEventScenarioScopingTests +{ + [Fact] + public async Task WithMatchingScopedAndGlobalSubscribedSetups_OnlyScopedShouldFire() + { + int scopedCount = 0; + int globalCount = 0; + IScenarioService sut = IScenarioService.CreateMock(); + + sut.Mock.InScenario("a").Setup.Event.OnSubscribed.Do(() => { scopedCount++; }); + sut.Mock.Setup.Event.OnSubscribed.Do(() => { globalCount++; }); + sut.Mock.TransitionTo("a"); + + sut.Event += (_, _) => { }; + + await That(scopedCount).IsEqualTo(1); + await That(globalCount).IsEqualTo(0); + } + + [Fact] + public async Task WithMatchingScopedAndGlobalUnsubscribedSetups_OnlyScopedShouldFire() + { + int scopedCount = 0; + int globalCount = 0; + IScenarioService sut = IScenarioService.CreateMock(); + + sut.Mock.InScenario("a").Setup.Event.OnUnsubscribed.Do(() => { scopedCount++; }); + sut.Mock.Setup.Event.OnUnsubscribed.Do(() => { globalCount++; }); + sut.Mock.TransitionTo("a"); + + sut.Event -= (_, _) => { }; + + await That(scopedCount).IsEqualTo(1); + await That(globalCount).IsEqualTo(0); + } + + [Fact] + public async Task WithScopedBucketButNoScopedEventSetup_GlobalShouldStillFire() + { + int globalCount = 0; + IScenarioService sut = IScenarioService.CreateMock(); + + // Register something in scope "a" that isn't the event so the scoped bucket exists + // but has no OnSubscribed setups. + sut.Mock.InScenario("a").Setup.Property.OnGet.Do(() => { }); + sut.Mock.Setup.Event.OnSubscribed.Do(() => { globalCount++; }); + sut.Mock.TransitionTo("a"); + + sut.Event += (_, _) => { }; + + await That(globalCount).IsEqualTo(1); + } +} From ef09f75ac5eafb6f05df4c0b69a9e8fed8af7355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Wed, 22 Apr 2026 21:40:49 +0200 Subject: [PATCH 09/13] test: kill Awaitable verify recording-disabled and Verified-tracking mutants The Awaitable sibling of VerificationResult has its own copy of the recording-disabled guard and the Verified() tracking call that the non-Awaitable path already has tests for. Hitting the Within() overload takes the Awaitable branch: verify it throws on SkippingInteractionRecording and that it marks matching interactions as verified for GetUnverifiedInteractions. Stryker-disable three further mutants that have no observable effect: ConfigureAwait(false) (no SynchronizationContext in tests), and the strict-less-than/ternary-fallback in the in-order verifier where position uniqueness and the int.MaxValue seed make the two branches equivalent. Co-Authored-By: Claude Opus 4.7 (1M context) --- Source/Mockolate/Verify/VerificationResult.cs | 1 + .../Verify/VerificationResultExtensions.cs | 2 ++ .../Verify/VerificationResultMutationTests.cs | 36 +++++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 Tests/Mockolate.Tests/Verify/VerificationResultMutationTests.cs diff --git a/Source/Mockolate/Verify/VerificationResult.cs b/Source/Mockolate/Verify/VerificationResult.cs index 24455e9e..f9fd4910 100644 --- a/Source/Mockolate/Verify/VerificationResult.cs +++ b/Source/Mockolate/Verify/VerificationResult.cs @@ -160,6 +160,7 @@ bool IVerificationResult.Verify(Func predicate) return true; } + // Stryker disable once Boolean : ConfigureAwait(true) vs (false) is only observable in a synchronization context; Mockolate's test targets never exercise one, so the flag is equivalent in practice. return VerifyAsync(predicate).ConfigureAwait(false).GetAwaiter().GetResult(); } diff --git a/Source/Mockolate/Verify/VerificationResultExtensions.cs b/Source/Mockolate/Verify/VerificationResultExtensions.cs index a12012f2..54b7a8f7 100644 --- a/Source/Mockolate/Verify/VerificationResultExtensions.cs +++ b/Source/Mockolate/Verify/VerificationResultExtensions.cs @@ -471,6 +471,7 @@ bool VerifyInteractions(IInteraction[] interactions, IVerificationResult current IInteraction? firstInteraction = null; foreach (IInteraction candidate in interactions) { + // Stryker disable once Equality : positions are unique per interaction, so position <= bestPosition can only differ from position < bestPosition on exact equality, which never occurs. if (positions.TryGetValue(candidate, out int position) && position > after && position < bestPosition) @@ -481,6 +482,7 @@ bool VerifyInteractions(IInteraction[] interactions, IVerificationResult current } bool hasInteractionAfter = firstInteraction is not null; + // Stryker disable once Conditional : when hasInteractionAfter is false, bestPosition is still the int.MaxValue seed, so the ternary branches produce identical values. after = hasInteractionAfter ? bestPosition : int.MaxValue; if (!hasInteractionAfter && error is null) { diff --git a/Tests/Mockolate.Tests/Verify/VerificationResultMutationTests.cs b/Tests/Mockolate.Tests/Verify/VerificationResultMutationTests.cs new file mode 100644 index 00000000..34e37be2 --- /dev/null +++ b/Tests/Mockolate.Tests/Verify/VerificationResultMutationTests.cs @@ -0,0 +1,36 @@ +using aweXpect.Chronology; +using Mockolate.Exceptions; +using Mockolate.Interactions; +using Mockolate.Tests.TestHelpers; +using Mockolate.Verify; + +namespace Mockolate.Tests.Verify; + +public sealed class VerificationResultMutationTests +{ + [Fact] + public async Task AwaitableVerify_WithRecordingDisabled_ShouldThrowMockException() + { + IMyService sut = IMyService.CreateMock(MockBehavior.Default.SkippingInteractionRecording()); + sut.Mock.Setup.SomeFlag.InitializeWith(true); + _ = sut.SomeFlag; + + void Act() => sut.Mock.Verify.SomeFlag.Got().Within(10.Milliseconds()).Exactly(1); + + await That(Act).Throws() + .WithMessage("*interaction recording is disabled*").AsWildcard(); + } + + [Fact] + public async Task AwaitableVerify_AfterMatching_ShouldMarkMatchingInteractionsAsVerified() + { + IMyService sut = IMyService.CreateMock(); + sut.Mock.Setup.SomeFlag.InitializeWith(true); + _ = sut.SomeFlag; + + sut.Mock.Verify.SomeFlag.Got().Within(10.Milliseconds()).Exactly(1); + + MockInteractions interactions = ((IMock)sut).MockRegistry.Interactions; + await That(interactions.GetUnverifiedInteractions()).IsEmpty(); + } +} From 20cfa222313362954acb78b5a0a7253a0d48a424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Wed, 22 Apr 2026 21:48:46 +0200 Subject: [PATCH 10/13] test: kill web extension and HttpClient verify expectation mutants - HttpClient.Verify.{Delete,Patch,Put}: assert that a failing verification mentions SendAsync in the error message so the expectation factory string survives (pinning 6 "SendAsync" interpolations). - ItExtensions.IsHttpContent.WithBytes: calling WithBytes twice should leave the first predicate in force (??= semantics), so verifying with a matching first predicate and a non-matching second one must still succeed. - ItExtensions.IsHttpContent: multiple WithString predicates must be joined with " and " in the failure message, pinning the string.Join separator. - ItExtensions.IsUri: a request URI with a trailing slash must still match an exact-pattern without one - pins the TrimEnd('/') retry and the EndsWith/negation wrapping it. - Stryker-disable the HttpRequestMessageParameter.ToString list seed, which is equivalent to [] because the Where filter strips the null default entry. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Web/ItExtensions.HttpRequestMessage.cs | 1 + ...onsTests.Verify.ExpectationMessageTests.cs | 83 +++++++++++++++++++ .../Web/ItExtensionsTests.MutationTests.cs | 56 +++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 Tests/Mockolate.Tests/Web/HttpClientExtensionsTests.Verify.ExpectationMessageTests.cs create mode 100644 Tests/Mockolate.Tests/Web/ItExtensionsTests.MutationTests.cs diff --git a/Source/Mockolate/Web/ItExtensions.HttpRequestMessage.cs b/Source/Mockolate/Web/ItExtensions.HttpRequestMessage.cs index 3bcfc53b..cc96f717 100644 --- a/Source/Mockolate/Web/ItExtensions.HttpRequestMessage.cs +++ b/Source/Mockolate/Web/ItExtensions.HttpRequestMessage.cs @@ -176,6 +176,7 @@ protected virtual bool Matches(HttpRequestMessage value) /// public override string ToString() { + // Stryker disable once Collection : the Where(!string.IsNullOrEmpty) filter at the return strips any default null entry, so seeding the list with [default] produces the same output as the empty seed. List parts = []; if (_uriParameter is not null) { diff --git a/Tests/Mockolate.Tests/Web/HttpClientExtensionsTests.Verify.ExpectationMessageTests.cs b/Tests/Mockolate.Tests/Web/HttpClientExtensionsTests.Verify.ExpectationMessageTests.cs new file mode 100644 index 00000000..1ae6e30a --- /dev/null +++ b/Tests/Mockolate.Tests/Web/HttpClientExtensionsTests.Verify.ExpectationMessageTests.cs @@ -0,0 +1,83 @@ +using System.Net.Http; +using Mockolate.Exceptions; +using Mockolate.Verify; +using Mockolate.Web; + +namespace Mockolate.Tests.Web; + +public sealed class HttpClientExtensionsVerifyExpectationMessageTests +{ + [Fact] + public async Task DeleteAsync_StringUri_FailingVerification_ShouldMentionSendAsyncInMessage() + { + HttpClient httpClient = HttpClient.CreateMock(); + + void Act() + => httpClient.Mock.Verify.DeleteAsync(It.Matches("*aweXpect*")).AtLeastOnce(); + + await That(Act).Throws() + .WithMessage("*SendAsync*").AsWildcard(); + } + + [Fact] + public async Task DeleteAsync_Uri_FailingVerification_ShouldMentionSendAsyncInMessage() + { + HttpClient httpClient = HttpClient.CreateMock(); + + void Act() + => httpClient.Mock.Verify.DeleteAsync(It.IsUri("*aweXpect*")).AtLeastOnce(); + + await That(Act).Throws() + .WithMessage("*SendAsync*").AsWildcard(); + } + +#if NET8_0_OR_GREATER + [Fact] + public async Task PatchAsync_StringUri_FailingVerification_ShouldMentionSendAsyncInMessage() + { + HttpClient httpClient = HttpClient.CreateMock(); + + void Act() + => httpClient.Mock.Verify.PatchAsync(It.Matches("*aweXpect*"), It.IsAny()).AtLeastOnce(); + + await That(Act).Throws() + .WithMessage("*SendAsync*").AsWildcard(); + } + + [Fact] + public async Task PatchAsync_Uri_FailingVerification_ShouldMentionSendAsyncInMessage() + { + HttpClient httpClient = HttpClient.CreateMock(); + + void Act() + => httpClient.Mock.Verify.PatchAsync(It.IsUri("*aweXpect*"), It.IsAny()).AtLeastOnce(); + + await That(Act).Throws() + .WithMessage("*SendAsync*").AsWildcard(); + } +#endif + + [Fact] + public async Task PutAsync_StringUri_FailingVerification_ShouldMentionSendAsyncInMessage() + { + HttpClient httpClient = HttpClient.CreateMock(); + + void Act() + => httpClient.Mock.Verify.PutAsync(It.Matches("*aweXpect*"), It.IsAny()).AtLeastOnce(); + + await That(Act).Throws() + .WithMessage("*SendAsync*").AsWildcard(); + } + + [Fact] + public async Task PutAsync_Uri_FailingVerification_ShouldMentionSendAsyncInMessage() + { + HttpClient httpClient = HttpClient.CreateMock(); + + void Act() + => httpClient.Mock.Verify.PutAsync(It.IsUri("*aweXpect*"), It.IsAny()).AtLeastOnce(); + + await That(Act).Throws() + .WithMessage("*SendAsync*").AsWildcard(); + } +} diff --git a/Tests/Mockolate.Tests/Web/ItExtensionsTests.MutationTests.cs b/Tests/Mockolate.Tests/Web/ItExtensionsTests.MutationTests.cs new file mode 100644 index 00000000..2bc3d0c5 --- /dev/null +++ b/Tests/Mockolate.Tests/Web/ItExtensionsTests.MutationTests.cs @@ -0,0 +1,56 @@ +using System.Net.Http; +using Mockolate.Exceptions; +using Mockolate.Verify; +using Mockolate.Web; + +namespace Mockolate.Tests.Web; + +public sealed class ItExtensionsMutationTests +{ + [Fact] + public async Task IsHttpContent_WithBytes_CalledTwice_ShouldUseFirstPredicate() + { + HttpClient httpClient = HttpClient.CreateMock(); + byte[] payload = { 1, 2, 3, }; + + await httpClient.PostAsync("https://aweXpect.com", new ByteArrayContent(payload)); + + // The first WithBytes predicate matches (length == 3) while the second does not. + // Original (??=): first wins, verification succeeds. + // Mutant (=): second wins, verification fails. + await That(httpClient.Mock.Verify.PostAsync( + It.IsAny(), + It.IsHttpContent() + .WithBytes(b => b.Length == 3) + .WithBytes(b => b.Length == 99))) + .Once(); + } + + [Fact] + public async Task IsHttpContent_WithMultipleWithString_FailingVerification_ShouldJoinPredicatesWithAnd() + { + HttpClient httpClient = HttpClient.CreateMock(); + + void Act() + => httpClient.Mock.Verify.PostAsync( + It.IsAny(), + It.IsHttpContent() + .WithString(s => s.StartsWith("a")) + .WithString(s => s.EndsWith("z"))) + .AtLeastOnce(); + + await That(Act).Throws() + .WithMessage("*and*").AsWildcard(); + } + + [Fact] + public async Task IsUri_WithTrailingSlashOnRequestAndNoSuffixWildcardInPattern_ShouldStillMatch() + { + HttpClient httpClient = HttpClient.CreateMock(); + + await httpClient.GetAsync("https://aweXpect.com/"); + + await That(httpClient.Mock.Verify.GetAsync(It.IsUri("https://aweXpect.com"))) + .Once(); + } +} From 836808402a6e16c8b173c6ee7513bfe3fd662632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Breu=C3=9F=20Valentin?= Date: Thu, 23 Apr 2026 07:50:23 +0200 Subject: [PATCH 11/13] Fix review issues --- .../Mockolate/Setup/MockSetups.Properties.cs | 12 +- Source/Mockolate/Verify/VerificationResult.cs | 1 - .../SetupMutationTests.cs | 6 +- .../SetupEventTests.ForMutationTests.cs | 50 ------ .../SetupEventTests.InScenarioTests.cs | 61 +++++++ .../SetupEventTests.ScenarioScopingTests.cs | 57 ------- .../MockEvents/SetupEventTests.cs | 46 +++++- ...s => SetupIndexerTests.InScenarioTests.cs} | 2 +- ...onsTests.Verify.ExpectationMessageTests.cs | 156 ++++++++++-------- ...Tests.IsHttpContentTests.WithBytesTests.cs | 25 ++- ...ests.IsHttpContentTests.WithStringTests.cs | 21 +++ .../Web/ItExtensionsTests.IsUriTests.cs | 11 ++ .../Web/ItExtensionsTests.MutationTests.cs | 56 ------- 13 files changed, 263 insertions(+), 241 deletions(-) delete mode 100644 Tests/Mockolate.Tests/MockEvents/SetupEventTests.ForMutationTests.cs create mode 100644 Tests/Mockolate.Tests/MockEvents/SetupEventTests.InScenarioTests.cs delete mode 100644 Tests/Mockolate.Tests/MockEvents/SetupEventTests.ScenarioScopingTests.cs rename Tests/Mockolate.Tests/MockIndexers/{SetupIndexerTests.TransitionToInParallelTests.cs => SetupIndexerTests.InScenarioTests.cs} (98%) delete mode 100644 Tests/Mockolate.Tests/Web/ItExtensionsTests.MutationTests.cs diff --git a/Source/Mockolate/Setup/MockSetups.Properties.cs b/Source/Mockolate/Setup/MockSetups.Properties.cs index ed3c3e81..f1b5a42e 100644 --- a/Source/Mockolate/Setup/MockSetups.Properties.cs +++ b/Source/Mockolate/Setup/MockSetups.Properties.cs @@ -32,8 +32,16 @@ public void Add(PropertySetup setup) lock (_writeLock) { PropertySetup[] old = _storage ?? []; - int existingIndex = Array.FindIndex(old, - s => string.Equals(s.Name, setup.Name, StringComparison.Ordinal)); + int existingIndex = -1; + string setupName = setup.Name; + for (int i = 0; i < old.Length; i++) + { + if (string.Equals(old[i].Name, setupName, StringComparison.Ordinal)) + { + existingIndex = i; + break; + } + } if (existingIndex >= 0) { diff --git a/Source/Mockolate/Verify/VerificationResult.cs b/Source/Mockolate/Verify/VerificationResult.cs index f9fd4910..24455e9e 100644 --- a/Source/Mockolate/Verify/VerificationResult.cs +++ b/Source/Mockolate/Verify/VerificationResult.cs @@ -160,7 +160,6 @@ bool IVerificationResult.Verify(Func predicate) return true; } - // Stryker disable once Boolean : ConfigureAwait(true) vs (false) is only observable in a synchronization context; Mockolate's test targets never exercise one, so the flag is equivalent in practice. return VerifyAsync(predicate).ConfigureAwait(false).GetAwaiter().GetResult(); } diff --git a/Tests/Mockolate.Internal.Tests/SetupMutationTests.cs b/Tests/Mockolate.Internal.Tests/SetupMutationTests.cs index 46fd8a49..15f70b55 100644 --- a/Tests/Mockolate.Internal.Tests/SetupMutationTests.cs +++ b/Tests/Mockolate.Internal.Tests/SetupMutationTests.cs @@ -113,7 +113,7 @@ public async Task AutoInitializeWith_WhenAlreadyInitialized_ShouldNotOverwriteVa interactive.InitializeWith(5); interactive.InitializeWith(10); - int value = interactive.InvokeGetter(null, MockBehavior.Default, () => -1); + int value = interactive.InvokeGetter(null, MockBehavior.Default, () => -1); await That(value).IsEqualTo(5); } @@ -126,7 +126,7 @@ public async Task UserInitializeWith_SecondCall_ShouldNotOverwriteValue() userFacing.InitializeWith(5); userFacing.InitializeWith(10); - int value = ((IInteractivePropertySetup)setup).InvokeGetter(null, MockBehavior.Default, () => -1); + int value = ((IInteractivePropertySetup)setup).InvokeGetter(null, MockBehavior.Default, () => -1); await That(value).IsEqualTo(5); } @@ -138,7 +138,7 @@ public async Task DefaultInvokeGetter_WhenRequestedTypeDiffersFromBackingType_Sh PropertySetup.Default setup = new("p", 0x40400000); IInteractivePropertySetup interactive = setup; - float value = interactive.InvokeGetter(null, MockBehavior.Default, () => 99f); + float value = interactive.InvokeGetter(null, MockBehavior.Default, () => 99f); await That(value).IsEqualTo(99f); } diff --git a/Tests/Mockolate.Tests/MockEvents/SetupEventTests.ForMutationTests.cs b/Tests/Mockolate.Tests/MockEvents/SetupEventTests.ForMutationTests.cs deleted file mode 100644 index e3b38abb..00000000 --- a/Tests/Mockolate.Tests/MockEvents/SetupEventTests.ForMutationTests.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Mockolate.Tests.TestHelpers; - -namespace Mockolate.Tests.MockEvents; - -public sealed class SetupEventForMutationTests -{ - [Fact] - public async Task OnSubscribed_For2_HoldsFirstCallbackForTwoSubscriptionsBeforeAdvancing() - { - int count1 = 0; - int count2 = 0; - IChocolateDispenser sut = IChocolateDispenser.CreateMock(); - - sut.Mock.Setup.ChocolateDispensed - .OnSubscribed.Do(() => { count1++; }).For(2) - .OnSubscribed.Do(() => { count2++; }); - - sut.ChocolateDispensed += Handler; - sut.ChocolateDispensed += Handler; - sut.ChocolateDispensed += Handler; - sut.ChocolateDispensed += Handler; - - await That(count1).IsEqualTo(3); - await That(count2).IsEqualTo(1); - - void Handler(string type, int amount) { } - } - - [Fact] - public async Task OnUnsubscribed_For2_HoldsFirstCallbackForTwoUnsubscriptionsBeforeAdvancing() - { - int count1 = 0; - int count2 = 0; - IChocolateDispenser sut = IChocolateDispenser.CreateMock(); - - sut.Mock.Setup.ChocolateDispensed - .OnUnsubscribed.Do(() => { count1++; }).For(2) - .OnUnsubscribed.Do(() => { count2++; }); - - sut.ChocolateDispensed -= Handler; - sut.ChocolateDispensed -= Handler; - sut.ChocolateDispensed -= Handler; - sut.ChocolateDispensed -= Handler; - - await That(count1).IsEqualTo(3); - await That(count2).IsEqualTo(1); - - void Handler(string type, int amount) { } - } -} diff --git a/Tests/Mockolate.Tests/MockEvents/SetupEventTests.InScenarioTests.cs b/Tests/Mockolate.Tests/MockEvents/SetupEventTests.InScenarioTests.cs new file mode 100644 index 00000000..f0acc633 --- /dev/null +++ b/Tests/Mockolate.Tests/MockEvents/SetupEventTests.InScenarioTests.cs @@ -0,0 +1,61 @@ +using Mockolate.Tests.TestHelpers; + +namespace Mockolate.Tests.MockEvents; + +public sealed partial class SetupEventTests +{ + public sealed class InScenarioTests + { + [Fact] + public async Task WithMatchingScopedAndGlobalSubscribedSetups_OnlyScopedShouldFire() + { + int scopedCount = 0; + int globalCount = 0; + IScenarioService sut = IScenarioService.CreateMock(); + + sut.Mock.InScenario("a").Setup.Event.OnSubscribed.Do(() => { scopedCount++; }); + sut.Mock.Setup.Event.OnSubscribed.Do(() => { globalCount++; }); + sut.Mock.TransitionTo("a"); + + sut.Event += (_, _) => { }; + + await That(scopedCount).IsEqualTo(1); + await That(globalCount).IsEqualTo(0); + } + + [Fact] + public async Task WithMatchingScopedAndGlobalUnsubscribedSetups_OnlyScopedShouldFire() + { + int scopedCount = 0; + int globalCount = 0; + IScenarioService sut = IScenarioService.CreateMock(); + + sut.Mock.InScenario("a").Setup.Event.OnUnsubscribed.Do(() => { scopedCount++; }); + sut.Mock.Setup.Event.OnUnsubscribed.Do(() => { globalCount++; }); + sut.Mock.TransitionTo("a"); + + // ReSharper disable once EventUnsubscriptionViaAnonymousDelegate + sut.Event -= (_, _) => { }; + + await That(scopedCount).IsEqualTo(1); + await That(globalCount).IsEqualTo(0); + } + + [Fact] + public async Task WithScopedBucketButNoScopedEventSetup_GlobalShouldStillFire() + { + int globalCount = 0; + IScenarioService sut = IScenarioService.CreateMock(); + + // Register something in scope "a" that isn't the event so the scoped bucket exists + // but has no OnSubscribed setups. + sut.Mock.InScenario("a").Setup.Property.OnGet.Do(() => { }); + sut.Mock.Setup.Event.OnSubscribed.Do(() => { globalCount++; }); + sut.Mock.TransitionTo("a"); + + sut.Event += (_, _) => { }; + + await That(globalCount).IsEqualTo(1); + } + } +} diff --git a/Tests/Mockolate.Tests/MockEvents/SetupEventTests.ScenarioScopingTests.cs b/Tests/Mockolate.Tests/MockEvents/SetupEventTests.ScenarioScopingTests.cs deleted file mode 100644 index 5d07b87d..00000000 --- a/Tests/Mockolate.Tests/MockEvents/SetupEventTests.ScenarioScopingTests.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Mockolate.Tests.TestHelpers; - -namespace Mockolate.Tests.MockEvents; - -public sealed class SetupEventScenarioScopingTests -{ - [Fact] - public async Task WithMatchingScopedAndGlobalSubscribedSetups_OnlyScopedShouldFire() - { - int scopedCount = 0; - int globalCount = 0; - IScenarioService sut = IScenarioService.CreateMock(); - - sut.Mock.InScenario("a").Setup.Event.OnSubscribed.Do(() => { scopedCount++; }); - sut.Mock.Setup.Event.OnSubscribed.Do(() => { globalCount++; }); - sut.Mock.TransitionTo("a"); - - sut.Event += (_, _) => { }; - - await That(scopedCount).IsEqualTo(1); - await That(globalCount).IsEqualTo(0); - } - - [Fact] - public async Task WithMatchingScopedAndGlobalUnsubscribedSetups_OnlyScopedShouldFire() - { - int scopedCount = 0; - int globalCount = 0; - IScenarioService sut = IScenarioService.CreateMock(); - - sut.Mock.InScenario("a").Setup.Event.OnUnsubscribed.Do(() => { scopedCount++; }); - sut.Mock.Setup.Event.OnUnsubscribed.Do(() => { globalCount++; }); - sut.Mock.TransitionTo("a"); - - sut.Event -= (_, _) => { }; - - await That(scopedCount).IsEqualTo(1); - await That(globalCount).IsEqualTo(0); - } - - [Fact] - public async Task WithScopedBucketButNoScopedEventSetup_GlobalShouldStillFire() - { - int globalCount = 0; - IScenarioService sut = IScenarioService.CreateMock(); - - // Register something in scope "a" that isn't the event so the scoped bucket exists - // but has no OnSubscribed setups. - sut.Mock.InScenario("a").Setup.Property.OnGet.Do(() => { }); - sut.Mock.Setup.Event.OnSubscribed.Do(() => { globalCount++; }); - sut.Mock.TransitionTo("a"); - - sut.Event += (_, _) => { }; - - await That(globalCount).IsEqualTo(1); - } -} diff --git a/Tests/Mockolate.Tests/MockEvents/SetupEventTests.cs b/Tests/Mockolate.Tests/MockEvents/SetupEventTests.cs index 8621422f..48ba9a3f 100644 --- a/Tests/Mockolate.Tests/MockEvents/SetupEventTests.cs +++ b/Tests/Mockolate.Tests/MockEvents/SetupEventTests.cs @@ -4,7 +4,7 @@ namespace Mockolate.Tests.MockEvents; -public sealed class SetupEventTests +public sealed partial class SetupEventTests { [Fact] public async Task Forever_Extension_RepeatsIndefinitely() @@ -128,6 +128,28 @@ public async Task OnSubscribed_For_RepeatsCallbackNTimes() void Handler(string type, int amount) { } } + [Fact] + public async Task OnSubscribed_For2_HoldsFirstCallbackForTwoSubscriptionsBeforeAdvancing() + { + int count1 = 0; + int count2 = 0; + IChocolateDispenser sut = IChocolateDispenser.CreateMock(); + + sut.Mock.Setup.ChocolateDispensed + .OnSubscribed.Do(() => { count1++; }).For(2) + .OnSubscribed.Do(() => { count2++; }); + + sut.ChocolateDispensed += Handler; + sut.ChocolateDispensed += Handler; + sut.ChocolateDispensed += Handler; + sut.ChocolateDispensed += Handler; + + await That(count1).IsEqualTo(3); + await That(count2).IsEqualTo(1); + + void Handler(string type, int amount) { } + } + [Fact] public async Task OnSubscribed_InParallel_RunsAlongsideNextCallback() { @@ -254,6 +276,28 @@ void Handler(string type, int amount) { } await That(count2).IsEqualTo(1); } + [Fact] + public async Task OnUnsubscribed_For2_HoldsFirstCallbackForTwoUnsubscriptionsBeforeAdvancing() + { + int count1 = 0; + int count2 = 0; + IChocolateDispenser sut = IChocolateDispenser.CreateMock(); + + sut.Mock.Setup.ChocolateDispensed + .OnUnsubscribed.Do(() => { count1++; }).For(2) + .OnUnsubscribed.Do(() => { count2++; }); + + sut.ChocolateDispensed -= Handler; + sut.ChocolateDispensed -= Handler; + sut.ChocolateDispensed -= Handler; + sut.ChocolateDispensed -= Handler; + + await That(count1).IsEqualTo(3); + await That(count2).IsEqualTo(1); + + void Handler(string type, int amount) { } + } + [Fact] public async Task OnUnsubscribed_Forever_Extension_RepeatsIndefinitely() { diff --git a/Tests/Mockolate.Tests/MockIndexers/SetupIndexerTests.TransitionToInParallelTests.cs b/Tests/Mockolate.Tests/MockIndexers/SetupIndexerTests.InScenarioTests.cs similarity index 98% rename from Tests/Mockolate.Tests/MockIndexers/SetupIndexerTests.TransitionToInParallelTests.cs rename to Tests/Mockolate.Tests/MockIndexers/SetupIndexerTests.InScenarioTests.cs index e8bed6b5..107b4ccc 100644 --- a/Tests/Mockolate.Tests/MockIndexers/SetupIndexerTests.TransitionToInParallelTests.cs +++ b/Tests/Mockolate.Tests/MockIndexers/SetupIndexerTests.InScenarioTests.cs @@ -4,7 +4,7 @@ namespace Mockolate.Tests.MockIndexers; public sealed partial class SetupIndexerTests { - public sealed class TransitionToInParallelTests + public sealed class InScenarioTests { [Fact] public async Task OnGet_Arity1_ShouldTransitionEvenAfterPrecedingCallbackRan() diff --git a/Tests/Mockolate.Tests/Web/HttpClientExtensionsTests.Verify.ExpectationMessageTests.cs b/Tests/Mockolate.Tests/Web/HttpClientExtensionsTests.Verify.ExpectationMessageTests.cs index 1ae6e30a..716036a3 100644 --- a/Tests/Mockolate.Tests/Web/HttpClientExtensionsTests.Verify.ExpectationMessageTests.cs +++ b/Tests/Mockolate.Tests/Web/HttpClientExtensionsTests.Verify.ExpectationMessageTests.cs @@ -5,79 +5,97 @@ namespace Mockolate.Tests.Web; -public sealed class HttpClientExtensionsVerifyExpectationMessageTests +public sealed partial class HttpClientExtensionsTests { - [Fact] - public async Task DeleteAsync_StringUri_FailingVerification_ShouldMentionSendAsyncInMessage() + public sealed partial class Verify { - HttpClient httpClient = HttpClient.CreateMock(); - - void Act() - => httpClient.Mock.Verify.DeleteAsync(It.Matches("*aweXpect*")).AtLeastOnce(); - - await That(Act).Throws() - .WithMessage("*SendAsync*").AsWildcard(); - } - - [Fact] - public async Task DeleteAsync_Uri_FailingVerification_ShouldMentionSendAsyncInMessage() - { - HttpClient httpClient = HttpClient.CreateMock(); - - void Act() - => httpClient.Mock.Verify.DeleteAsync(It.IsUri("*aweXpect*")).AtLeastOnce(); - - await That(Act).Throws() - .WithMessage("*SendAsync*").AsWildcard(); - } + public sealed class ExpectationMessageTests + { + [Fact] + public async Task DeleteAsync_StringUri_FailingVerification_ShouldMentionSendAsyncInMessage() + { + HttpClient httpClient = HttpClient.CreateMock(); + + void Act() + { + httpClient.Mock.Verify.DeleteAsync(It.Matches("*aweXpect*")).AtLeastOnce(); + } + + await That(Act).Throws() + .WithMessage("*SendAsync*").AsWildcard(); + } + + [Fact] + public async Task DeleteAsync_Uri_FailingVerification_ShouldMentionSendAsyncInMessage() + { + HttpClient httpClient = HttpClient.CreateMock(); + + void Act() + { + httpClient.Mock.Verify.DeleteAsync(It.IsUri("*aweXpect*")).AtLeastOnce(); + } + + await That(Act).Throws() + .WithMessage("*SendAsync*").AsWildcard(); + } + + [Fact] + public async Task PutAsync_StringUri_FailingVerification_ShouldMentionSendAsyncInMessage() + { + HttpClient httpClient = HttpClient.CreateMock(); + + void Act() + { + httpClient.Mock.Verify.PutAsync(It.Matches("*aweXpect*"), It.IsAny()).AtLeastOnce(); + } + + await That(Act).Throws() + .WithMessage("*SendAsync*").AsWildcard(); + } + + [Fact] + public async Task PutAsync_Uri_FailingVerification_ShouldMentionSendAsyncInMessage() + { + HttpClient httpClient = HttpClient.CreateMock(); + + void Act() + { + httpClient.Mock.Verify.PutAsync(It.IsUri("*aweXpect*"), It.IsAny()).AtLeastOnce(); + } + + await That(Act).Throws() + .WithMessage("*SendAsync*").AsWildcard(); + } #if NET8_0_OR_GREATER - [Fact] - public async Task PatchAsync_StringUri_FailingVerification_ShouldMentionSendAsyncInMessage() - { - HttpClient httpClient = HttpClient.CreateMock(); - - void Act() - => httpClient.Mock.Verify.PatchAsync(It.Matches("*aweXpect*"), It.IsAny()).AtLeastOnce(); - - await That(Act).Throws() - .WithMessage("*SendAsync*").AsWildcard(); - } - - [Fact] - public async Task PatchAsync_Uri_FailingVerification_ShouldMentionSendAsyncInMessage() - { - HttpClient httpClient = HttpClient.CreateMock(); - - void Act() - => httpClient.Mock.Verify.PatchAsync(It.IsUri("*aweXpect*"), It.IsAny()).AtLeastOnce(); - - await That(Act).Throws() - .WithMessage("*SendAsync*").AsWildcard(); - } + [Fact] + public async Task PatchAsync_StringUri_FailingVerification_ShouldMentionSendAsyncInMessage() + { + HttpClient httpClient = HttpClient.CreateMock(); + + void Act() + { + httpClient.Mock.Verify.PatchAsync(It.Matches("*aweXpect*"), It.IsAny()).AtLeastOnce(); + } + + await That(Act).Throws() + .WithMessage("*SendAsync*").AsWildcard(); + } + + [Fact] + public async Task PatchAsync_Uri_FailingVerification_ShouldMentionSendAsyncInMessage() + { + HttpClient httpClient = HttpClient.CreateMock(); + + void Act() + { + httpClient.Mock.Verify.PatchAsync(It.IsUri("*aweXpect*"), It.IsAny()).AtLeastOnce(); + } + + await That(Act).Throws() + .WithMessage("*SendAsync*").AsWildcard(); + } #endif - - [Fact] - public async Task PutAsync_StringUri_FailingVerification_ShouldMentionSendAsyncInMessage() - { - HttpClient httpClient = HttpClient.CreateMock(); - - void Act() - => httpClient.Mock.Verify.PutAsync(It.Matches("*aweXpect*"), It.IsAny()).AtLeastOnce(); - - await That(Act).Throws() - .WithMessage("*SendAsync*").AsWildcard(); - } - - [Fact] - public async Task PutAsync_Uri_FailingVerification_ShouldMentionSendAsyncInMessage() - { - HttpClient httpClient = HttpClient.CreateMock(); - - void Act() - => httpClient.Mock.Verify.PutAsync(It.IsUri("*aweXpect*"), It.IsAny()).AtLeastOnce(); - - await That(Act).Throws() - .WithMessage("*SendAsync*").AsWildcard(); + } } } diff --git a/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithBytesTests.cs b/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithBytesTests.cs index 0a34fe7a..ac535692 100644 --- a/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithBytesTests.cs +++ b/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithBytesTests.cs @@ -11,6 +11,28 @@ public sealed partial class IsHttpContentTests { public sealed class WithBytesTests { + [Fact] + public async Task CalledTwice_ShouldUseFirstPredicate() + { + HttpClient httpClient = HttpClient.CreateMock(); + byte[] payload = + { + 1, 2, 3, + }; + + await httpClient.PostAsync("https://aweXpect.com", new ByteArrayContent(payload), CancellationToken.None); + + // The first WithBytes predicate matches (length == 3) while the second does not. + // Original (??=): first wins, verification succeeds. + // Mutant (=): second wins, verification fails. + await That(httpClient.Mock.Verify.PostAsync( + It.IsAny(), + It.IsHttpContent() + .WithBytes(b => b.Length == 3) + .WithBytes(b => b.Length == 99))) + .Once(); + } + [Theory] [InlineData(new byte[0], 0x1, false)] [InlineData(new byte[] @@ -37,7 +59,8 @@ public async Task Predicate_ShouldValidatePredicate(byte[] body, byte expectedFi { HttpClient httpClient = HttpClient.CreateMock(); httpClient.Mock.Setup - .PostAsync(It.IsAny(), It.IsHttpContent().WithBytes(b => b.Length > 0 && b[0] == expectedFirstByte)) + .PostAsync(It.IsAny(), + It.IsHttpContent().WithBytes(b => b.Length > 0 && b[0] == expectedFirstByte)) .ReturnsAsync(HttpStatusCode.OK); HttpResponseMessage result = await httpClient.PostAsync("https://www.aweXpect.com", diff --git a/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithStringTests.cs b/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithStringTests.cs index e0d4fa6a..1a6cc196 100644 --- a/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithStringTests.cs +++ b/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithStringTests.cs @@ -5,7 +5,9 @@ using System.Net.Http.Headers; using System.Text; using System.Threading; +using Mockolate.Exceptions; using Mockolate.Parameters; +using Mockolate.Verify; using Mockolate.Web; namespace Mockolate.Tests.Web; @@ -77,6 +79,25 @@ await That(result.StatusCode) .IsEqualTo(expectSuccess ? HttpStatusCode.OK : HttpStatusCode.NotImplemented); } + [Fact] + public async Task WithMultipleWithString_FailingVerification_ShouldJoinPredicatesWithAnd() + { + HttpClient httpClient = HttpClient.CreateMock(); + + void Act() + { + httpClient.Mock.Verify.PostAsync( + It.IsAny(), + It.IsHttpContent() + .WithString(s => s.StartsWith("abc")) + .WithString(s => s.EndsWith("xyz"))) + .AtLeastOnce(); + } + + await That(Act).Throws() + .WithMessage("s => s.StartsWith(\"abc\") and s => s.EndsWith(\"xyz\")"); + } + [Theory] [InlineData("", true)] [InlineData("foo", true)] diff --git a/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsUriTests.cs b/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsUriTests.cs index 25beaf8f..6f40a839 100644 --- a/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsUriTests.cs +++ b/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsUriTests.cs @@ -99,5 +99,16 @@ public async Task WhenTypeDoesNotMatch_Null_ShouldReturnFalse() await That(result).IsFalse(); } + + [Fact] + public async Task WithTrailingSlashOnRequestAndNoSuffixWildcardInPattern_ShouldStillMatch() + { + HttpClient httpClient = HttpClient.CreateMock(); + + await httpClient.GetAsync("https://aweXpect.com/", CancellationToken.None); + + await That(httpClient.Mock.Verify.GetAsync(It.IsUri("https://aweXpect.com"))) + .Once(); + } } } diff --git a/Tests/Mockolate.Tests/Web/ItExtensionsTests.MutationTests.cs b/Tests/Mockolate.Tests/Web/ItExtensionsTests.MutationTests.cs deleted file mode 100644 index 2bc3d0c5..00000000 --- a/Tests/Mockolate.Tests/Web/ItExtensionsTests.MutationTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Net.Http; -using Mockolate.Exceptions; -using Mockolate.Verify; -using Mockolate.Web; - -namespace Mockolate.Tests.Web; - -public sealed class ItExtensionsMutationTests -{ - [Fact] - public async Task IsHttpContent_WithBytes_CalledTwice_ShouldUseFirstPredicate() - { - HttpClient httpClient = HttpClient.CreateMock(); - byte[] payload = { 1, 2, 3, }; - - await httpClient.PostAsync("https://aweXpect.com", new ByteArrayContent(payload)); - - // The first WithBytes predicate matches (length == 3) while the second does not. - // Original (??=): first wins, verification succeeds. - // Mutant (=): second wins, verification fails. - await That(httpClient.Mock.Verify.PostAsync( - It.IsAny(), - It.IsHttpContent() - .WithBytes(b => b.Length == 3) - .WithBytes(b => b.Length == 99))) - .Once(); - } - - [Fact] - public async Task IsHttpContent_WithMultipleWithString_FailingVerification_ShouldJoinPredicatesWithAnd() - { - HttpClient httpClient = HttpClient.CreateMock(); - - void Act() - => httpClient.Mock.Verify.PostAsync( - It.IsAny(), - It.IsHttpContent() - .WithString(s => s.StartsWith("a")) - .WithString(s => s.EndsWith("z"))) - .AtLeastOnce(); - - await That(Act).Throws() - .WithMessage("*and*").AsWildcard(); - } - - [Fact] - public async Task IsUri_WithTrailingSlashOnRequestAndNoSuffixWildcardInPattern_ShouldStillMatch() - { - HttpClient httpClient = HttpClient.CreateMock(); - - await httpClient.GetAsync("https://aweXpect.com/"); - - await That(httpClient.Mock.Verify.GetAsync(It.IsUri("https://aweXpect.com"))) - .Once(); - } -} From abd5faaaa2986cd77d900c377c74a4430d8d74f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Breu=C3=9F=20Valentin?= Date: Thu, 23 Apr 2026 08:19:00 +0200 Subject: [PATCH 12/13] Refactor internal tests --- .../Behavior/MockBehaviorTests.cs | 22 ++ .../MockVerificationTimeoutExceptionTests.cs | 2 +- .../IndexerSetupMutationTests.cs | 131 ------------ .../IndexerAccessTests.cs} | 192 +++++++++++------- .../Interactions/MethodInvocationTests.cs | 30 +++ .../MockInteractionsTests.cs | 2 +- .../{ => Internals}/StringExtensionsTests.cs | 2 +- .../{ => Internals}/TypeFormatterTests.cs | 2 +- .../{ => Internals}/WildcardTests.cs | 2 +- .../ParameterMatchAdapterTests.cs | 2 +- .../MockRegistryTests.cs} | 111 ++++------ .../{ => Setup}/CallbackTests.cs | 88 +++++++- .../IndexerSetupTests.cs} | 157 +++++++++++++- .../MockSetupsTests.IndexersTests.cs} | 15 +- .../MockSetupsTests.MethodsTests.cs} | 16 +- .../MockSetupsTests.PropertiesTests.cs} | 21 +- .../{ => Setup}/MockSetupsTests.cs | 5 +- .../Setup/PropertySetupTests.cs | 46 +++++ .../SetupMutationTests.cs | 171 ---------------- .../TestHelpers/FakeIndexerAccess.cs | 3 +- .../TestHelpers/FakeIndexerSetup.cs | 3 +- .../HttpClientExtensionsTests.cs} | 4 +- ...ests.IsHttpContentTests.WithStringTests.cs | 2 +- 23 files changed, 551 insertions(+), 478 deletions(-) create mode 100644 Tests/Mockolate.Internal.Tests/Behavior/MockBehaviorTests.cs rename Tests/Mockolate.Internal.Tests/{ => Exceptions}/MockVerificationTimeoutExceptionTests.cs (94%) delete mode 100644 Tests/Mockolate.Internal.Tests/IndexerSetupMutationTests.cs rename Tests/Mockolate.Internal.Tests/{IndexerAccessWhiteBoxTests.cs => Interactions/IndexerAccessTests.cs} (81%) create mode 100644 Tests/Mockolate.Internal.Tests/Interactions/MethodInvocationTests.cs rename Tests/Mockolate.Internal.Tests/{ => Interactions}/MockInteractionsTests.cs (94%) rename Tests/Mockolate.Internal.Tests/{ => Internals}/StringExtensionsTests.cs (96%) rename Tests/Mockolate.Internal.Tests/{ => Internals}/TypeFormatterTests.cs (95%) rename Tests/Mockolate.Internal.Tests/{ => Internals}/WildcardTests.cs (89%) rename Tests/Mockolate.Internal.Tests/{ => Parameters}/ParameterMatchAdapterTests.cs (95%) rename Tests/Mockolate.Internal.Tests/{MockRegistryMutationTests.cs => Registry/MockRegistryTests.cs} (59%) rename Tests/Mockolate.Internal.Tests/{ => Setup}/CallbackTests.cs (76%) rename Tests/Mockolate.Internal.Tests/{IndexerSetupWhiteBoxTests.cs => Setup/IndexerSetupTests.cs} (82%) rename Tests/Mockolate.Internal.Tests/{MockSetupsTests.IndexerSetupsTests.cs => Setup/MockSetupsTests.IndexersTests.cs} (78%) rename Tests/Mockolate.Internal.Tests/{MockSetupsTests.MethodSetupsTests.cs => Setup/MockSetupsTests.MethodsTests.cs} (82%) rename Tests/Mockolate.Internal.Tests/{MockSetupsTests.PropertySetupsTests.cs => Setup/MockSetupsTests.PropertiesTests.cs} (75%) rename Tests/Mockolate.Internal.Tests/{ => Setup}/MockSetupsTests.cs (91%) create mode 100644 Tests/Mockolate.Internal.Tests/Setup/PropertySetupTests.cs delete mode 100644 Tests/Mockolate.Internal.Tests/SetupMutationTests.cs rename Tests/Mockolate.Internal.Tests/{WebTests.cs => Web/HttpClientExtensionsTests.cs} (96%) diff --git a/Tests/Mockolate.Internal.Tests/Behavior/MockBehaviorTests.cs b/Tests/Mockolate.Internal.Tests/Behavior/MockBehaviorTests.cs new file mode 100644 index 00000000..a84cb90c --- /dev/null +++ b/Tests/Mockolate.Internal.Tests/Behavior/MockBehaviorTests.cs @@ -0,0 +1,22 @@ +namespace Mockolate.Internal.Tests.Behavior; + +public sealed class MockBehaviorTests +{ + public sealed class ToStringTests + { + [Fact] + public async Task WithMultipleFlags_ShouldKeepAllPartsInOutput() + { + MockBehavior behavior = MockBehavior.Default + .ThrowingWhenNotSetup() + .SkippingBaseClass() + .SkippingInteractionRecording(); + + string result = behavior.ToString(); + + await That(result).Contains("ThrowingWhenNotSetup"); + await That(result).Contains("SkippingBaseClass"); + await That(result).Contains("SkippingInteractionRecording"); + } + } +} diff --git a/Tests/Mockolate.Internal.Tests/MockVerificationTimeoutExceptionTests.cs b/Tests/Mockolate.Internal.Tests/Exceptions/MockVerificationTimeoutExceptionTests.cs similarity index 94% rename from Tests/Mockolate.Internal.Tests/MockVerificationTimeoutExceptionTests.cs rename to Tests/Mockolate.Internal.Tests/Exceptions/MockVerificationTimeoutExceptionTests.cs index 03d8f8d2..e1759726 100644 --- a/Tests/Mockolate.Internal.Tests/MockVerificationTimeoutExceptionTests.cs +++ b/Tests/Mockolate.Internal.Tests/Exceptions/MockVerificationTimeoutExceptionTests.cs @@ -1,7 +1,7 @@ using aweXpect.Chronology; using Mockolate.Exceptions; -namespace Mockolate.Internal.Tests; +namespace Mockolate.Internal.Tests.Exceptions; public class MockVerificationTimeoutExceptionTests { diff --git a/Tests/Mockolate.Internal.Tests/IndexerSetupMutationTests.cs b/Tests/Mockolate.Internal.Tests/IndexerSetupMutationTests.cs deleted file mode 100644 index c58e98f9..00000000 --- a/Tests/Mockolate.Internal.Tests/IndexerSetupMutationTests.cs +++ /dev/null @@ -1,131 +0,0 @@ -using Mockolate.Interactions; -using Mockolate.Internal.Tests.TestHelpers; -using Mockolate.Parameters; -using Mockolate.Setup; - -namespace Mockolate.Internal.Tests; - -public sealed class IndexerSetupMutationTests -{ - [Fact] - public async Task TryCast_WhenValueIsNull_ShouldReturnTrue() - { - bool success = FakeIndexerSetup.InvokeTryCast(null, out string result, MockBehavior.Default); - - await That(success).IsTrue(); - await That(result).IsNull(); - } - - [Fact] - public async Task TryCast_WhenValueIsNotOfTargetTypeAndNotNull_ShouldReturnFalse() - { - bool success = FakeIndexerSetup.InvokeTryCast(42, out string _, MockBehavior.Default); - - await That(success).IsFalse(); - } - - [Fact] - public async Task GetResult_WithBaseValue_1Param_StoresComputedValueForLaterLookup() - { - IndexerSetup setup = new( - new MockRegistry(MockBehavior.Default), - (IParameterMatch)It.IsAny()); - IndexerValueStorage storage = new(); - IndexerGetterAccess access1 = new("p", 42) { Storage = storage, }; - - string result = setup.GetResult(access1, MockBehavior.Default, "base"); - - IndexerGetterAccess access2 = new("p", 42) { Storage = storage, }; - bool found = access2.TryFindStoredValue(out string stored); - - await That(result).IsEqualTo("base"); - await That(found).IsTrue(); - await That(stored).IsEqualTo("base"); - } - - [Fact] - public async Task GetResult_WithDefaultValueGenerator_1Param_StoresComputedValueForLaterLookup() - { - IndexerSetup setup = new( - new MockRegistry(MockBehavior.Default), - (IParameterMatch)It.IsAny()); - IndexerValueStorage storage = new(); - IndexerGetterAccess access1 = new("p", 42) { Storage = storage, }; - - string result = setup.GetResult(access1, MockBehavior.Default, () => "generated"); - - IndexerGetterAccess access2 = new("p", 42) { Storage = storage, }; - bool found = access2.TryFindStoredValue(out string stored); - - await That(result).IsEqualTo("generated"); - await That(found).IsTrue(); - await That(stored).IsEqualTo("generated"); - } - - [Fact] - public async Task GetResult_WithBaseValue_2Param_StoresComputedValueForLaterLookup() - { - IndexerSetup setup = new( - new MockRegistry(MockBehavior.Default), - (IParameterMatch)It.IsAny(), - (IParameterMatch)It.IsAny()); - IndexerValueStorage storage = new(); - IndexerGetterAccess access1 = new("p1", 1, "p2", 2) { Storage = storage, }; - - string result = setup.GetResult(access1, MockBehavior.Default, "base"); - - IndexerGetterAccess access2 = new("p1", 1, "p2", 2) { Storage = storage, }; - bool found = access2.TryFindStoredValue(out string stored); - - await That(result).IsEqualTo("base"); - await That(found).IsTrue(); - await That(stored).IsEqualTo("base"); - } - - [Fact] - public async Task GetResult_WithBaseValue_3Param_StoresComputedValueForLaterLookup() - { - IndexerSetup setup = new( - new MockRegistry(MockBehavior.Default), - (IParameterMatch)It.IsAny(), - (IParameterMatch)It.IsAny(), - (IParameterMatch)It.IsAny()); - IndexerValueStorage storage = new(); - IndexerGetterAccess access1 = - new("p1", 1, "p2", 2, "p3", 3) { Storage = storage, }; - - string result = setup.GetResult(access1, MockBehavior.Default, "base"); - - IndexerGetterAccess access2 = - new("p1", 1, "p2", 2, "p3", 3) { Storage = storage, }; - bool found = access2.TryFindStoredValue(out string stored); - - await That(result).IsEqualTo("base"); - await That(found).IsTrue(); - await That(stored).IsEqualTo("base"); - } - - [Fact] - public async Task GetResult_WithBaseValue_4Param_StoresComputedValueForLaterLookup() - { - IndexerSetup setup = new( - new MockRegistry(MockBehavior.Default), - (IParameterMatch)It.IsAny(), - (IParameterMatch)It.IsAny(), - (IParameterMatch)It.IsAny(), - (IParameterMatch)It.IsAny()); - IndexerValueStorage storage = new(); - IndexerGetterAccess access1 = - new("p1", 1, "p2", 2, "p3", 3, "p4", 4) { Storage = storage, }; - - string result = setup.GetResult(access1, MockBehavior.Default, "base"); - - IndexerGetterAccess access2 = - new("p1", 1, "p2", 2, "p3", 3, "p4", 4) { Storage = storage, }; - bool found = access2.TryFindStoredValue(out string stored); - - await That(result).IsEqualTo("base"); - await That(found).IsTrue(); - await That(stored).IsEqualTo("base"); - } -} diff --git a/Tests/Mockolate.Internal.Tests/IndexerAccessWhiteBoxTests.cs b/Tests/Mockolate.Internal.Tests/Interactions/IndexerAccessTests.cs similarity index 81% rename from Tests/Mockolate.Internal.Tests/IndexerAccessWhiteBoxTests.cs rename to Tests/Mockolate.Internal.Tests/Interactions/IndexerAccessTests.cs index c503474a..17859451 100644 --- a/Tests/Mockolate.Internal.Tests/IndexerAccessWhiteBoxTests.cs +++ b/Tests/Mockolate.Internal.Tests/Interactions/IndexerAccessTests.cs @@ -2,60 +2,75 @@ using Mockolate.Interactions; using Mockolate.Setup; -namespace Mockolate.Internal.Tests; +namespace Mockolate.Internal.Tests.Interactions; -public sealed class IndexerAccessWhiteBoxTests +public sealed class IndexerAccessTests { [Fact] - public async Task TryFindStoredValue_OnIndexerGetterAccess1_UsesGetChildDispatch() + public async Task StoreValue_OnIndexerGetterAccess1_UsesGetOrAddChildDispatch() { RecordingStorage storage = new(); - IndexerGetterAccess access = new("p1", 42) { Storage = storage, }; + IndexerGetterAccess access = new("p1", 42) + { + Storage = storage, + }; - access.TryFindStoredValue(out string _); + access.StoreValue("v"); - await That(storage.Calls).IsEqualTo(["Get(42)",]); + await That(storage.Calls).IsEqualTo(["GetOrAdd(42)",]); } [Fact] - public async Task TryFindStoredValue_OnIndexerGetterAccess2_UsesGetChildDispatchForEachParameter() + public async Task StoreValue_OnIndexerGetterAccess2_UsesGetOrAddChildDispatchForEachParameter() { RecordingStorage storage = new(); - IndexerGetterAccess access = new("p1", 1, "p2", 2) { Storage = storage, }; + IndexerGetterAccess access = new("p1", 1, "p2", 2) + { + Storage = storage, + }; - access.TryFindStoredValue(out string _); + access.StoreValue("v"); - await That(storage.Calls).IsEqualTo(["Get(1)", "Get(2)",]); + await That(storage.Calls).IsEqualTo(["GetOrAdd(1)", "GetOrAdd(2)",]); } [Fact] - public async Task TryFindStoredValue_OnIndexerGetterAccess3_UsesGetChildDispatchForEachParameter() + public async Task StoreValue_OnIndexerGetterAccess3_UsesGetOrAddChildDispatchForEachParameter() { RecordingStorage storage = new(); - IndexerGetterAccess access = new("p1", 1, "p2", 2, "p3", 3) { Storage = storage, }; + IndexerGetterAccess access = new("p1", 1, "p2", 2, "p3", 3) + { + Storage = storage, + }; - access.TryFindStoredValue(out string _); + access.StoreValue("v"); - await That(storage.Calls).IsEqualTo(["Get(1)", "Get(2)", "Get(3)",]); + await That(storage.Calls).IsEqualTo(["GetOrAdd(1)", "GetOrAdd(2)", "GetOrAdd(3)",]); } [Fact] - public async Task TryFindStoredValue_OnIndexerGetterAccess4_UsesGetChildDispatchForEachParameter() + public async Task StoreValue_OnIndexerGetterAccess4_UsesGetOrAddChildDispatchForEachParameter() { RecordingStorage storage = new(); IndexerGetterAccess access = - new("p1", 1, "p2", 2, "p3", 3, "p4", 4) { Storage = storage, }; + new("p1", 1, "p2", 2, "p3", 3, "p4", 4) + { + Storage = storage, + }; - access.TryFindStoredValue(out string _); + access.StoreValue("v"); - await That(storage.Calls).IsEqualTo(["Get(1)", "Get(2)", "Get(3)", "Get(4)",]); + await That(storage.Calls).IsEqualTo(["GetOrAdd(1)", "GetOrAdd(2)", "GetOrAdd(3)", "GetOrAdd(4)",]); } [Fact] - public async Task StoreValue_OnIndexerGetterAccess1_UsesGetOrAddChildDispatch() + public async Task StoreValue_OnIndexerSetterAccess1_UsesGetOrAddChildDispatch() { RecordingStorage storage = new(); - IndexerGetterAccess access = new("p1", 42) { Storage = storage, }; + IndexerSetterAccess access = new("p1", 42, "v") + { + Storage = storage, + }; access.StoreValue("v"); @@ -63,10 +78,14 @@ public async Task StoreValue_OnIndexerGetterAccess1_UsesGetOrAddChildDispatch() } [Fact] - public async Task StoreValue_OnIndexerGetterAccess2_UsesGetOrAddChildDispatchForEachParameter() + public async Task StoreValue_OnIndexerSetterAccess2_UsesGetOrAddChildDispatchForEachParameter() { RecordingStorage storage = new(); - IndexerGetterAccess access = new("p1", 1, "p2", 2) { Storage = storage, }; + IndexerSetterAccess access = + new("p1", 1, "p2", 2, "v") + { + Storage = storage, + }; access.StoreValue("v"); @@ -74,10 +93,14 @@ public async Task StoreValue_OnIndexerGetterAccess2_UsesGetOrAddChildDispatchFor } [Fact] - public async Task StoreValue_OnIndexerGetterAccess3_UsesGetOrAddChildDispatchForEachParameter() + public async Task StoreValue_OnIndexerSetterAccess3_UsesGetOrAddChildDispatchForEachParameter() { RecordingStorage storage = new(); - IndexerGetterAccess access = new("p1", 1, "p2", 2, "p3", 3) { Storage = storage, }; + IndexerSetterAccess access = + new("p1", 1, "p2", 2, "p3", 3, "v") + { + Storage = storage, + }; access.StoreValue("v"); @@ -85,11 +108,14 @@ public async Task StoreValue_OnIndexerGetterAccess3_UsesGetOrAddChildDispatchFor } [Fact] - public async Task StoreValue_OnIndexerGetterAccess4_UsesGetOrAddChildDispatchForEachParameter() + public async Task StoreValue_OnIndexerSetterAccess4_UsesGetOrAddChildDispatchForEachParameter() { RecordingStorage storage = new(); - IndexerGetterAccess access = - new("p1", 1, "p2", 2, "p3", 3, "p4", 4) { Storage = storage, }; + IndexerSetterAccess access = + new("p1", 1, "p2", 2, "p3", 3, "p4", 4, "v") + { + Storage = storage, + }; access.StoreValue("v"); @@ -97,10 +123,31 @@ public async Task StoreValue_OnIndexerGetterAccess4_UsesGetOrAddChildDispatchFor } [Fact] - public async Task TryFindStoredValue_OnIndexerSetterAccess1_UsesGetChildDispatch() + public async Task ToString_OnIndexerGetterAccess4_FormatsEachNullParameterAsNullLiteral() + { + IndexerGetterAccess access = + new("p1", null, "p2", null, "p3", null, "p4", null); + + await That(access.ToString()).IsEqualTo("get indexer [null, null, null, null]"); + } + + [Fact] + public async Task ToString_OnIndexerSetterAccess4_FormatsEachNullParameterAsNullLiteral() + { + IndexerSetterAccess access = + new("p1", null, "p2", null, "p3", null, "p4", null, null); + + await That(access.ToString()).IsEqualTo("set indexer [null, null, null, null] to null"); + } + + [Fact] + public async Task TryFindStoredValue_OnIndexerGetterAccess1_UsesGetChildDispatch() { RecordingStorage storage = new(); - IndexerSetterAccess access = new("p1", 42, "v") { Storage = storage, }; + IndexerGetterAccess access = new("p1", 42) + { + Storage = storage, + }; access.TryFindStoredValue(out string _); @@ -108,11 +155,13 @@ public async Task TryFindStoredValue_OnIndexerSetterAccess1_UsesGetChildDispatch } [Fact] - public async Task TryFindStoredValue_OnIndexerSetterAccess2_UsesGetChildDispatchForEachParameter() + public async Task TryFindStoredValue_OnIndexerGetterAccess2_UsesGetChildDispatchForEachParameter() { RecordingStorage storage = new(); - IndexerSetterAccess access = - new("p1", 1, "p2", 2, "v") { Storage = storage, }; + IndexerGetterAccess access = new("p1", 1, "p2", 2) + { + Storage = storage, + }; access.TryFindStoredValue(out string _); @@ -120,11 +169,13 @@ public async Task TryFindStoredValue_OnIndexerSetterAccess2_UsesGetChildDispatch } [Fact] - public async Task TryFindStoredValue_OnIndexerSetterAccess3_UsesGetChildDispatchForEachParameter() + public async Task TryFindStoredValue_OnIndexerGetterAccess3_UsesGetChildDispatchForEachParameter() { RecordingStorage storage = new(); - IndexerSetterAccess access = - new("p1", 1, "p2", 2, "p3", 3, "v") { Storage = storage, }; + IndexerGetterAccess access = new("p1", 1, "p2", 2, "p3", 3) + { + Storage = storage, + }; access.TryFindStoredValue(out string _); @@ -132,11 +183,14 @@ public async Task TryFindStoredValue_OnIndexerSetterAccess3_UsesGetChildDispatch } [Fact] - public async Task TryFindStoredValue_OnIndexerSetterAccess4_UsesGetChildDispatchForEachParameter() + public async Task TryFindStoredValue_OnIndexerGetterAccess4_UsesGetChildDispatchForEachParameter() { RecordingStorage storage = new(); - IndexerSetterAccess access = - new("p1", 1, "p2", 2, "p3", 3, "p4", 4, "v") { Storage = storage, }; + IndexerGetterAccess access = + new("p1", 1, "p2", 2, "p3", 3, "p4", 4) + { + Storage = storage, + }; access.TryFindStoredValue(out string _); @@ -144,68 +198,62 @@ public async Task TryFindStoredValue_OnIndexerSetterAccess4_UsesGetChildDispatch } [Fact] - public async Task StoreValue_OnIndexerSetterAccess1_UsesGetOrAddChildDispatch() + public async Task TryFindStoredValue_OnIndexerSetterAccess1_UsesGetChildDispatch() { RecordingStorage storage = new(); - IndexerSetterAccess access = new("p1", 42, "v") { Storage = storage, }; + IndexerSetterAccess access = new("p1", 42, "v") + { + Storage = storage, + }; - access.StoreValue("v"); + access.TryFindStoredValue(out string _); - await That(storage.Calls).IsEqualTo(["GetOrAdd(42)",]); + await That(storage.Calls).IsEqualTo(["Get(42)",]); } [Fact] - public async Task StoreValue_OnIndexerSetterAccess2_UsesGetOrAddChildDispatchForEachParameter() + public async Task TryFindStoredValue_OnIndexerSetterAccess2_UsesGetChildDispatchForEachParameter() { RecordingStorage storage = new(); IndexerSetterAccess access = - new("p1", 1, "p2", 2, "v") { Storage = storage, }; + new("p1", 1, "p2", 2, "v") + { + Storage = storage, + }; - access.StoreValue("v"); + access.TryFindStoredValue(out string _); - await That(storage.Calls).IsEqualTo(["GetOrAdd(1)", "GetOrAdd(2)",]); + await That(storage.Calls).IsEqualTo(["Get(1)", "Get(2)",]); } [Fact] - public async Task StoreValue_OnIndexerSetterAccess3_UsesGetOrAddChildDispatchForEachParameter() + public async Task TryFindStoredValue_OnIndexerSetterAccess3_UsesGetChildDispatchForEachParameter() { RecordingStorage storage = new(); IndexerSetterAccess access = - new("p1", 1, "p2", 2, "p3", 3, "v") { Storage = storage, }; + new("p1", 1, "p2", 2, "p3", 3, "v") + { + Storage = storage, + }; - access.StoreValue("v"); + access.TryFindStoredValue(out string _); - await That(storage.Calls).IsEqualTo(["GetOrAdd(1)", "GetOrAdd(2)", "GetOrAdd(3)",]); + await That(storage.Calls).IsEqualTo(["Get(1)", "Get(2)", "Get(3)",]); } [Fact] - public async Task StoreValue_OnIndexerSetterAccess4_UsesGetOrAddChildDispatchForEachParameter() + public async Task TryFindStoredValue_OnIndexerSetterAccess4_UsesGetChildDispatchForEachParameter() { RecordingStorage storage = new(); IndexerSetterAccess access = - new("p1", 1, "p2", 2, "p3", 3, "p4", 4, "v") { Storage = storage, }; + new("p1", 1, "p2", 2, "p3", 3, "p4", 4, "v") + { + Storage = storage, + }; - access.StoreValue("v"); - - await That(storage.Calls).IsEqualTo(["GetOrAdd(1)", "GetOrAdd(2)", "GetOrAdd(3)", "GetOrAdd(4)",]); - } - - [Fact] - public async Task ToString_OnIndexerGetterAccess4_FormatsEachNullParameterAsNullLiteral() - { - IndexerGetterAccess access = - new("p1", null, "p2", null, "p3", null, "p4", null); - - await That(access.ToString()).IsEqualTo("get indexer [null, null, null, null]"); - } - - [Fact] - public async Task ToString_OnIndexerSetterAccess4_FormatsEachNullParameterAsNullLiteral() - { - IndexerSetterAccess access = - new("p1", null, "p2", null, "p3", null, "p4", null, null); + access.TryFindStoredValue(out string _); - await That(access.ToString()).IsEqualTo("set indexer [null, null, null, null] to null"); + await That(storage.Calls).IsEqualTo(["Get(1)", "Get(2)", "Get(3)", "Get(4)",]); } private sealed class RecordingStorage : IndexerValueStorage diff --git a/Tests/Mockolate.Internal.Tests/Interactions/MethodInvocationTests.cs b/Tests/Mockolate.Internal.Tests/Interactions/MethodInvocationTests.cs new file mode 100644 index 00000000..1d89fdc5 --- /dev/null +++ b/Tests/Mockolate.Internal.Tests/Interactions/MethodInvocationTests.cs @@ -0,0 +1,30 @@ +using Mockolate.Interactions; + +namespace Mockolate.Internal.Tests.Interactions; + +public sealed class MethodInvocationTests +{ + public sealed class ThreeParameterToString + { + [Fact] + public async Task Should_Include_ThirdParameterValue() + { + MethodInvocation sut = new("MyType.MyMethod", "a", 1, "b", 2, "c", 3); + + string result = sut.ToString(); + + await That(result).IsEqualTo("invoke method MyMethod(1, 2, 3)"); + } + + [Fact] + public async Task WithNullThirdParameter_Should_FormatAsNullLiteral() + { + MethodInvocation sut = + new("MyType.MyMethod", "a", null, "b", null, "c", null); + + string result = sut.ToString(); + + await That(result).IsEqualTo("invoke method MyMethod(null, null, null)"); + } + } +} diff --git a/Tests/Mockolate.Internal.Tests/MockInteractionsTests.cs b/Tests/Mockolate.Internal.Tests/Interactions/MockInteractionsTests.cs similarity index 94% rename from Tests/Mockolate.Internal.Tests/MockInteractionsTests.cs rename to Tests/Mockolate.Internal.Tests/Interactions/MockInteractionsTests.cs index e3f330a1..4ac1e71f 100644 --- a/Tests/Mockolate.Internal.Tests/MockInteractionsTests.cs +++ b/Tests/Mockolate.Internal.Tests/Interactions/MockInteractionsTests.cs @@ -1,6 +1,6 @@ using Mockolate.Interactions; -namespace Mockolate.Internal.Tests; +namespace Mockolate.Internal.Tests.Interactions; public class MockInteractionsTests { diff --git a/Tests/Mockolate.Internal.Tests/StringExtensionsTests.cs b/Tests/Mockolate.Internal.Tests/Internals/StringExtensionsTests.cs similarity index 96% rename from Tests/Mockolate.Internal.Tests/StringExtensionsTests.cs rename to Tests/Mockolate.Internal.Tests/Internals/StringExtensionsTests.cs index d6ef9dd2..005a4659 100644 --- a/Tests/Mockolate.Internal.Tests/StringExtensionsTests.cs +++ b/Tests/Mockolate.Internal.Tests/Internals/StringExtensionsTests.cs @@ -1,6 +1,6 @@ using Mockolate.Internals; -namespace Mockolate.Internal.Tests; +namespace Mockolate.Internal.Tests.Internals; public sealed class StringExtensionsTests { diff --git a/Tests/Mockolate.Internal.Tests/TypeFormatterTests.cs b/Tests/Mockolate.Internal.Tests/Internals/TypeFormatterTests.cs similarity index 95% rename from Tests/Mockolate.Internal.Tests/TypeFormatterTests.cs rename to Tests/Mockolate.Internal.Tests/Internals/TypeFormatterTests.cs index e9648ccb..313ee16f 100644 --- a/Tests/Mockolate.Internal.Tests/TypeFormatterTests.cs +++ b/Tests/Mockolate.Internal.Tests/Internals/TypeFormatterTests.cs @@ -2,7 +2,7 @@ using System.Reflection; using Mockolate.Internals; -namespace Mockolate.Internal.Tests; +namespace Mockolate.Internal.Tests.Internals; public sealed class TypeFormatterTests { diff --git a/Tests/Mockolate.Internal.Tests/WildcardTests.cs b/Tests/Mockolate.Internal.Tests/Internals/WildcardTests.cs similarity index 89% rename from Tests/Mockolate.Internal.Tests/WildcardTests.cs rename to Tests/Mockolate.Internal.Tests/Internals/WildcardTests.cs index eacc75ab..05a793c3 100644 --- a/Tests/Mockolate.Internal.Tests/WildcardTests.cs +++ b/Tests/Mockolate.Internal.Tests/Internals/WildcardTests.cs @@ -1,6 +1,6 @@ using Mockolate.Internals; -namespace Mockolate.Internal.Tests; +namespace Mockolate.Internal.Tests.Internals; public class WildcardTests { diff --git a/Tests/Mockolate.Internal.Tests/ParameterMatchAdapterTests.cs b/Tests/Mockolate.Internal.Tests/Parameters/ParameterMatchAdapterTests.cs similarity index 95% rename from Tests/Mockolate.Internal.Tests/ParameterMatchAdapterTests.cs rename to Tests/Mockolate.Internal.Tests/Parameters/ParameterMatchAdapterTests.cs index 4818ced1..7b1108f3 100644 --- a/Tests/Mockolate.Internal.Tests/ParameterMatchAdapterTests.cs +++ b/Tests/Mockolate.Internal.Tests/Parameters/ParameterMatchAdapterTests.cs @@ -1,6 +1,6 @@ using Mockolate.Parameters; -namespace Mockolate.Internal.Tests; +namespace Mockolate.Internal.Tests.Parameters; public class ParameterMatchAdapterTests { diff --git a/Tests/Mockolate.Internal.Tests/MockRegistryMutationTests.cs b/Tests/Mockolate.Internal.Tests/Registry/MockRegistryTests.cs similarity index 59% rename from Tests/Mockolate.Internal.Tests/MockRegistryMutationTests.cs rename to Tests/Mockolate.Internal.Tests/Registry/MockRegistryTests.cs index ff4ad0b7..6ede380f 100644 --- a/Tests/Mockolate.Internal.Tests/MockRegistryMutationTests.cs +++ b/Tests/Mockolate.Internal.Tests/Registry/MockRegistryTests.cs @@ -1,21 +1,19 @@ -using System; -using System.Collections.Generic; using Mockolate.Interactions; using Mockolate.Internal.Tests.TestHelpers; using Mockolate.Setup; -namespace Mockolate.Internal.Tests; +namespace Mockolate.Internal.Tests.Registry; -public sealed class MockRegistryMutationTests +public sealed class MockRegistryTests { - public sealed class GetIndexerSetupScenarioScopingTests + public sealed class GetIndexerSetupScenarioScoping { [Fact] public async Task WithActiveScenario_ShouldReturnScopedSetupOverGlobalSetup() { MockRegistry registry = new(MockBehavior.Default); - FakeIndexerSetup globalSetup = new(match: true); - FakeIndexerSetup scopedSetup = new(match: true); + FakeIndexerSetup globalSetup = new(true); + FakeIndexerSetup scopedSetup = new(true); registry.Setup.Indexers.Add(globalSetup); registry.Setup.GetOrCreateScenario("a").Indexers.Add(scopedSetup); @@ -29,8 +27,8 @@ public async Task WithActiveScenario_ShouldReturnScopedSetupOverGlobalSetup() public async Task WithoutActiveScenario_ShouldFallBackToGlobalSetup() { MockRegistry registry = new(MockBehavior.Default); - FakeIndexerSetup globalSetup = new(match: true); - FakeIndexerSetup scopedSetup = new(match: true); + FakeIndexerSetup globalSetup = new(true); + FakeIndexerSetup scopedSetup = new(true); registry.Setup.Indexers.Add(globalSetup); registry.Setup.GetOrCreateScenario("a").Indexers.Add(scopedSetup); @@ -40,67 +38,70 @@ public async Task WithoutActiveScenario_ShouldFallBackToGlobalSetup() } } - - public sealed class IndexerFallbackStoresValueTests + public sealed class IndexerFallbackStoresValue { [Fact] - public async Task GetIndexerFallback_ShouldStoreDefaultForLaterLookup() + public async Task ApplyIndexerGetter_WithNullSetup_ShouldStoreBaseValueForLaterLookup() { - int counter = 0; - MockBehavior behavior = MockBehavior.Default.WithDefaultValueFor(() => ++counter); - MockRegistry registry = new(behavior); + MockRegistry registry = new(MockBehavior.Default); IndexerGetterAccess access1 = new("p", 1); IndexerGetterAccess access2 = new("p", 1); - int first = registry.GetIndexerFallback(access1, 0); - int second = registry.GetIndexerFallback(access2, 0); + int first = registry.ApplyIndexerGetter(access1, null, 42, 0); + int second = registry.ApplyIndexerGetter(access2, null, 99, 0); - await That(first).IsEqualTo(1); - await That(second).IsEqualTo(1); + await That(first).IsEqualTo(42); + await That(second).IsEqualTo(42); } [Fact] - public async Task ApplyIndexerGetter_WithNullSetup_ShouldStoreBaseValueForLaterLookup() + public async Task GetIndexerFallback_ShouldStoreDefaultForLaterLookup() { - MockRegistry registry = new(MockBehavior.Default); + int counter = 0; + MockBehavior behavior = MockBehavior.Default.WithDefaultValueFor(() => ++counter); + MockRegistry registry = new(behavior); IndexerGetterAccess access1 = new("p", 1); IndexerGetterAccess access2 = new("p", 1); - int first = registry.ApplyIndexerGetter(access1, null, 42, 0); - int second = registry.ApplyIndexerGetter(access2, null, 99, 0); + int first = registry.GetIndexerFallback(access1, 0); + int second = registry.GetIndexerFallback(access2, 0); - await That(first).IsEqualTo(42); - await That(second).IsEqualTo(42); + await That(first).IsEqualTo(1); + await That(second).IsEqualTo(1); } } - public sealed class InitializeStorageTests + public sealed class InitializeStorage { [Fact] - public async Task WithZero_ShouldNotThrow() + public async Task WithNegative_ShouldThrowWithDescriptiveMessage() { MockRegistry registry = new(MockBehavior.Default); void Act() - => registry.InitializeStorage(0); + { + registry.InitializeStorage(-1); + } - await That(Act).DoesNotThrow(); + await That(Act).Throws() + .WithMessage("*non-negative*").AsWildcard(); } [Fact] - public async Task WithNegative_ShouldThrowWithDescriptiveMessage() + public async Task WithZero_ShouldNotThrow() { MockRegistry registry = new(MockBehavior.Default); void Act() - => registry.InitializeStorage(-1); + { + registry.InitializeStorage(0); + } - await That(Act).Throws() - .WithMessage("*non-negative*").AsWildcard(); + await That(Act).DoesNotThrow(); } } - public sealed class WrapConstructorTests + public sealed class WrapConstructor { [Fact] public async Task WhenBehaviorSkipsInteractionRecording_WrappedRegistryAlsoSkipsRecording() @@ -117,46 +118,4 @@ public async Task WhenBehaviorSkipsInteractionRecording_WrappedRegistryAlsoSkips await That(wrappingRegistry.Interactions.Count).IsEqualTo(0); } } - - public sealed class MethodInvocationThreeParameterToStringTests - { - [Fact] - public async Task Should_Include_ThirdParameterValue() - { - MethodInvocation sut = new("MyType.MyMethod", "a", 1, "b", 2, "c", 3); - - string result = sut.ToString(); - - await That(result).IsEqualTo("invoke method MyMethod(1, 2, 3)"); - } - - [Fact] - public async Task WithNullThirdParameter_Should_FormatAsNullLiteral() - { - MethodInvocation sut = - new("MyType.MyMethod", "a", null, "b", null, "c", null); - - string result = sut.ToString(); - - await That(result).IsEqualTo("invoke method MyMethod(null, null, null)"); - } - } - - public sealed class MockBehaviorToStringTests - { - [Fact] - public async Task WithMultipleFlags_ShouldKeepAllPartsInOutput() - { - MockBehavior behavior = MockBehavior.Default - .ThrowingWhenNotSetup() - .SkippingBaseClass() - .SkippingInteractionRecording(); - - string result = behavior.ToString(); - - await That(result).Contains("ThrowingWhenNotSetup"); - await That(result).Contains("SkippingBaseClass"); - await That(result).Contains("SkippingInteractionRecording"); - } - } } diff --git a/Tests/Mockolate.Internal.Tests/CallbackTests.cs b/Tests/Mockolate.Internal.Tests/Setup/CallbackTests.cs similarity index 76% rename from Tests/Mockolate.Internal.Tests/CallbackTests.cs rename to Tests/Mockolate.Internal.Tests/Setup/CallbackTests.cs index eabf9482..15e3e1fd 100644 --- a/Tests/Mockolate.Internal.Tests/CallbackTests.cs +++ b/Tests/Mockolate.Internal.Tests/Setup/CallbackTests.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Mockolate.Setup; -namespace Mockolate.Internal.Tests; +namespace Mockolate.Internal.Tests.Setup; public class CallbackTests { @@ -138,6 +138,67 @@ public async Task WithForAndWhen_ShouldMatchInExpectedIterations( } } + public sealed class InvokeWithStateForCallbacksTests + { + [Fact] + public async Task ShouldIncludeIndexWhenMatching() + { + bool wasInvoked = false; + List values = []; + Callback sut = new(() => { }); + sut.Only(2); + sut.When(v => v > 1); + + int index = 0; + for (int i = 0; i < 5; i++) + { + sut.Invoke(wasInvoked, ref index, values, static (v, _, list) => list.Add(v)); + } + + await That(values).IsEqualTo([2, 3,]); + } + + [Theory] + [InlineData(2, 2, 0, 1, 1, 2, 2, 2, 2, 2, 2, 2)] + [InlineData(2, 3, 0, 1, 1, 2, 2, 3, 3, 3, 3, 3)] + public async Task ShouldIncrementIndexWheneverForIsExhausted( + int @for, int only, params int[] expectResult) + { + bool wasInvoked = false; + List indexValues = []; + Callback sut = new(() => { }); + sut.For(@for); + sut.Only(only); + + int index = 0; + for (int iteration = 1; iteration <= 10; iteration++) + { + sut.Invoke(wasInvoked, ref index, 0, static (_, _, _) => { }); + indexValues.Add(index); + } + + await That(indexValues).IsEqualTo(expectResult); + } + + [Fact] + public async Task ShouldLimitExecutionWhenRunningInParallel() + { + bool wasInvoked = false; + List values = []; + Callback sut = new(() => { }); + sut.Only(2); + sut.InParallel(); + + int index = 0; + for (int i = 0; i < 5; i++) + { + sut.Invoke(wasInvoked, ref index, values, static (v, _, list) => list.Add(v)); + } + + await That(values).IsEqualTo([0, 1,]); + } + } + public sealed class InvokeForVoidThrowsTests { [Fact] @@ -281,4 +342,29 @@ public async Task WithForAndWhen_ShouldMatchInExpectedIterations( await That(result).IsEqualTo(expectResult); } } + + public sealed class InvokeWithStateForReturnThrowsTests + { + [Fact] + public async Task ShouldIncludeIndexWhenMatching() + { + List values = []; + Callback sut = new(() => { }); + sut.Only(2); + sut.When(v => v > 1); + + int index = 0; + for (int i = 0; i < 5; i++) + { + sut.Invoke, string>(ref index, values, + static (v, _, list) => + { + list.Add(v); + return ""; + }, out _); + } + + await That(values).IsEqualTo([2, 3,]); + } + } } diff --git a/Tests/Mockolate.Internal.Tests/IndexerSetupWhiteBoxTests.cs b/Tests/Mockolate.Internal.Tests/Setup/IndexerSetupTests.cs similarity index 82% rename from Tests/Mockolate.Internal.Tests/IndexerSetupWhiteBoxTests.cs rename to Tests/Mockolate.Internal.Tests/Setup/IndexerSetupTests.cs index d69b75e6..efd94ecf 100644 --- a/Tests/Mockolate.Internal.Tests/IndexerSetupWhiteBoxTests.cs +++ b/Tests/Mockolate.Internal.Tests/Setup/IndexerSetupTests.cs @@ -1,10 +1,11 @@ using Mockolate.Interactions; +using Mockolate.Internal.Tests.TestHelpers; using Mockolate.Parameters; using Mockolate.Setup; -namespace Mockolate.Internal.Tests; +namespace Mockolate.Internal.Tests.Setup; -public sealed class IndexerSetupWhiteBoxTests +public sealed class IndexerSetupTests { [Fact] public async Task ExecuteGetterCallback_WhenGenericTypeDoesNotMatch_ShouldNotExecute() @@ -113,6 +114,73 @@ public async Task ExecuteSetterCallback_WhenTypesAndNumberMatch_ShouldExecute() await That(callCount).IsEqualTo(1); } + [Fact] + public async Task GetResult_WithBaseValue_StoresComputedValueForLaterLookup() + { + IndexerSetup setup = new( + new MockRegistry(MockBehavior.Default), + (IParameterMatch)It.IsAny()); + IndexerValueStorage storage = new(); + IndexerGetterAccess access1 = new("p", 42) + { + Storage = storage, + }; + + string result = setup.GetResult(access1, MockBehavior.Default, "base"); + + IndexerGetterAccess access2 = new("p", 42) + { + Storage = storage, + }; + bool found = access2.TryFindStoredValue(out string stored); + + await That(result).IsEqualTo("base"); + await That(found).IsTrue(); + await That(stored).IsEqualTo("base"); + } + + [Fact] + public async Task GetResult_WithDefaultValueGenerator_StoresComputedValueForLaterLookup() + { + IndexerSetup setup = new( + new MockRegistry(MockBehavior.Default), + (IParameterMatch)It.IsAny()); + IndexerValueStorage storage = new(); + IndexerGetterAccess access1 = new("p", 42) + { + Storage = storage, + }; + + string result = setup.GetResult(access1, MockBehavior.Default, () => "generated"); + + IndexerGetterAccess access2 = new("p", 42) + { + Storage = storage, + }; + bool found = access2.TryFindStoredValue(out string stored); + + await That(result).IsEqualTo("generated"); + await That(found).IsTrue(); + await That(stored).IsEqualTo("generated"); + } + + [Fact] + public async Task TryCast_WhenValueIsNotOfTargetTypeAndNotNull_ShouldReturnFalse() + { + bool success = FakeIndexerSetup.InvokeTryCast(42, out string _, MockBehavior.Default); + + await That(success).IsFalse(); + } + + [Fact] + public async Task TryCast_WhenValueIsNull_ShouldReturnTrue() + { + bool success = FakeIndexerSetup.InvokeTryCast(null, out string result, MockBehavior.Default); + + await That(success).IsTrue(); + await That(result).IsNull(); + } + public sealed class With2Levels { [Fact] @@ -222,6 +290,32 @@ public async Task ExecuteSetterCallback_WhenTypesAndNumberMatch_ShouldExecute() await That(callCount).IsEqualTo(1); } + [Fact] + public async Task GetResult_WithBaseValue_StoresComputedValueForLaterLookup() + { + IndexerSetup setup = new( + new MockRegistry(MockBehavior.Default), + (IParameterMatch)It.IsAny(), + (IParameterMatch)It.IsAny()); + IndexerValueStorage storage = new(); + IndexerGetterAccess access1 = new("p1", 1, "p2", 2) + { + Storage = storage, + }; + + string result = setup.GetResult(access1, MockBehavior.Default, "base"); + + IndexerGetterAccess access2 = new("p1", 1, "p2", 2) + { + Storage = storage, + }; + bool found = access2.TryFindStoredValue(out string stored); + + await That(result).IsEqualTo("base"); + await That(found).IsTrue(); + await That(stored).IsEqualTo("base"); + } + private sealed class MyIndexerSetup() : IndexerSetup( new MockRegistry(MockBehavior.Default), @@ -355,6 +449,35 @@ public async Task ExecuteSetterCallback_WhenTypesAndNumberMatch_ShouldExecute() await That(callCount).IsEqualTo(1); } + [Fact] + public async Task GetResult_WithBaseValue_StoresComputedValueForLaterLookup() + { + IndexerSetup setup = new( + new MockRegistry(MockBehavior.Default), + (IParameterMatch)It.IsAny(), + (IParameterMatch)It.IsAny(), + (IParameterMatch)It.IsAny()); + IndexerValueStorage storage = new(); + IndexerGetterAccess access1 = + new("p1", 1, "p2", 2, "p3", 3) + { + Storage = storage, + }; + + string result = setup.GetResult(access1, MockBehavior.Default, "base"); + + IndexerGetterAccess access2 = + new("p1", 1, "p2", 2, "p3", 3) + { + Storage = storage, + }; + bool found = access2.TryFindStoredValue(out string stored); + + await That(result).IsEqualTo("base"); + await That(found).IsTrue(); + await That(stored).IsEqualTo("base"); + } + private sealed class MyIndexerSetup() : IndexerSetup( new MockRegistry(MockBehavior.Default), @@ -496,6 +619,36 @@ public async Task ExecuteSetterCallback_WhenTypesAndNumberMatch_ShouldExecute() await That(callCount).IsEqualTo(1); } + [Fact] + public async Task GetResult_WithBaseValue_StoresComputedValueForLaterLookup() + { + IndexerSetup setup = new( + new MockRegistry(MockBehavior.Default), + (IParameterMatch)It.IsAny(), + (IParameterMatch)It.IsAny(), + (IParameterMatch)It.IsAny(), + (IParameterMatch)It.IsAny()); + IndexerValueStorage storage = new(); + IndexerGetterAccess access1 = + new("p1", 1, "p2", 2, "p3", 3, "p4", 4) + { + Storage = storage, + }; + + string result = setup.GetResult(access1, MockBehavior.Default, "base"); + + IndexerGetterAccess access2 = + new("p1", 1, "p2", 2, "p3", 3, "p4", 4) + { + Storage = storage, + }; + bool found = access2.TryFindStoredValue(out string stored); + + await That(result).IsEqualTo("base"); + await That(found).IsTrue(); + await That(stored).IsEqualTo("base"); + } + private sealed class MyIndexerSetup() : IndexerSetup( new MockRegistry(MockBehavior.Default), diff --git a/Tests/Mockolate.Internal.Tests/MockSetupsTests.IndexerSetupsTests.cs b/Tests/Mockolate.Internal.Tests/Setup/MockSetupsTests.IndexersTests.cs similarity index 78% rename from Tests/Mockolate.Internal.Tests/MockSetupsTests.IndexerSetupsTests.cs rename to Tests/Mockolate.Internal.Tests/Setup/MockSetupsTests.IndexersTests.cs index f7c036c3..310f806b 100644 --- a/Tests/Mockolate.Internal.Tests/MockSetupsTests.IndexerSetupsTests.cs +++ b/Tests/Mockolate.Internal.Tests/Setup/MockSetupsTests.IndexersTests.cs @@ -1,11 +1,11 @@ using Mockolate.Internal.Tests.TestHelpers; using Mockolate.Setup; -namespace Mockolate.Internal.Tests; +namespace Mockolate.Internal.Tests.Setup; public partial class MockSetupsTests { - public class IndexerSetupsTests + public class IndexersTests { [Fact] public async Task AddAndGetLatestOrDefault_ShouldReturnLatestMatching() @@ -17,8 +17,8 @@ public async Task AddAndGetLatestOrDefault_ShouldReturnLatestMatching() setups.Add(setup1); setups.Add(setup2); - FakeIndexerSetup? result = setups.GetMatching( - s => ((IInteractiveIndexerSetup)s).Matches(access)); + FakeIndexerSetup? result = + setups.GetMatching(s => ((IInteractiveIndexerSetup)s).Matches(access)); await That(result).IsEqualTo(setup1); } @@ -48,15 +48,14 @@ public async Task ThreadSafety_ConcurrentAddsAndQueries_ShouldReturnConsistentMa FakeIndexerSetup setup = new(shouldMatch); setups.Add(setup); FakeIndexerAccess access = new(); - _ = setups.GetMatching( - s => ((IInteractiveIndexerSetup)s).Matches(access)); + _ = setups.GetMatching(s => ((IInteractiveIndexerSetup)s).Matches(access)); }); FakeIndexerSetup finalMatch = new(true); setups.Add(finalMatch); FakeIndexerAccess finalAccess = new(); - FakeIndexerSetup? result = setups.GetMatching( - s => ((IInteractiveIndexerSetup)s).Matches(finalAccess)); + FakeIndexerSetup? result = + setups.GetMatching(s => ((IInteractiveIndexerSetup)s).Matches(finalAccess)); await That(result).IsEqualTo(finalMatch); } diff --git a/Tests/Mockolate.Internal.Tests/MockSetupsTests.MethodSetupsTests.cs b/Tests/Mockolate.Internal.Tests/Setup/MockSetupsTests.MethodsTests.cs similarity index 82% rename from Tests/Mockolate.Internal.Tests/MockSetupsTests.MethodSetupsTests.cs rename to Tests/Mockolate.Internal.Tests/Setup/MockSetupsTests.MethodsTests.cs index 5179617d..6b81ded9 100644 --- a/Tests/Mockolate.Internal.Tests/MockSetupsTests.MethodSetupsTests.cs +++ b/Tests/Mockolate.Internal.Tests/Setup/MockSetupsTests.MethodsTests.cs @@ -1,11 +1,11 @@ using Mockolate.Internal.Tests.TestHelpers; using Mockolate.Setup; -namespace Mockolate.Internal.Tests; +namespace Mockolate.Internal.Tests.Setup; public partial class MockSetupsTests { - public class MethodSetupsTests + public class MethodsTests { [Fact] public async Task AddAndRetrieve_ShouldReturnCorrectCount() @@ -34,6 +34,18 @@ public async Task GetLatestOrDefault_ShouldReturnLatestMatching() await That(result).IsEqualTo(setup2); } + [Fact] + public async Task GetLatestOrDefault_WithSingleMatchingSetup_ShouldReturnIt() + { + MockSetups.MethodSetups setups = new(); + FakeMethodSetup setup = new(); + setups.Add(setup); + + MethodSetup? result = setups.GetLatestOrDefault(_ => true); + + await That(result).IsSameAs(setup); + } + [Fact] public async Task Stress_ShouldMaintainCountAfterManyAdds() { diff --git a/Tests/Mockolate.Internal.Tests/MockSetupsTests.PropertySetupsTests.cs b/Tests/Mockolate.Internal.Tests/Setup/MockSetupsTests.PropertiesTests.cs similarity index 75% rename from Tests/Mockolate.Internal.Tests/MockSetupsTests.PropertySetupsTests.cs rename to Tests/Mockolate.Internal.Tests/Setup/MockSetupsTests.PropertiesTests.cs index 64300eb1..b22341c9 100644 --- a/Tests/Mockolate.Internal.Tests/MockSetupsTests.PropertySetupsTests.cs +++ b/Tests/Mockolate.Internal.Tests/Setup/MockSetupsTests.PropertiesTests.cs @@ -1,12 +1,29 @@ using Mockolate.Internal.Tests.TestHelpers; using Mockolate.Setup; -namespace Mockolate.Internal.Tests; +namespace Mockolate.Internal.Tests.Setup; public partial class MockSetupsTests { - public class PropertySetupsTests + public class PropertiesTests { + [Fact] + public async Task Add_ReplacingDefaultWithUserSetup_ShouldIncrementCountByOne() + { + MockSetups.PropertySetups setups = new(); + PropertySetup defaultSetup = new PropertySetup.Default("p", 0); + FakePropertySetup userSetup = new("p"); + + setups.Add(defaultSetup); + await That(setups.Count).IsEqualTo(0); + + setups.Add(userSetup); + + await That(setups.Count).IsEqualTo(1); + setups.TryGetValue("p", out PropertySetup? found); + await That(found).IsSameAs(userSetup); + } + [Fact] public async Task AddDuplicate_ShouldReplaceAndAdjustCount() { diff --git a/Tests/Mockolate.Internal.Tests/MockSetupsTests.cs b/Tests/Mockolate.Internal.Tests/Setup/MockSetupsTests.cs similarity index 91% rename from Tests/Mockolate.Internal.Tests/MockSetupsTests.cs rename to Tests/Mockolate.Internal.Tests/Setup/MockSetupsTests.cs index 93c45d86..98d5fe03 100644 --- a/Tests/Mockolate.Internal.Tests/MockSetupsTests.cs +++ b/Tests/Mockolate.Internal.Tests/Setup/MockSetupsTests.cs @@ -2,7 +2,7 @@ using Mockolate.Parameters; using Mockolate.Setup; -namespace Mockolate.Internal.Tests; +namespace Mockolate.Internal.Tests.Setup; public partial class MockSetupsTests { @@ -35,7 +35,8 @@ public async Task ToString_ShouldReturnExpectedValue( for (int i = 0; i < methodCount; i++) { - mock.MockRegistry.SetupMethod(new ReturnMethodSetup.WithParameterCollection(MockBehavior.Default, $"my.method{i}")); + mock.MockRegistry.SetupMethod( + new ReturnMethodSetup.WithParameterCollection(MockBehavior.Default, $"my.method{i}")); } for (int i = 0; i < propertyCount; i++) diff --git a/Tests/Mockolate.Internal.Tests/Setup/PropertySetupTests.cs b/Tests/Mockolate.Internal.Tests/Setup/PropertySetupTests.cs new file mode 100644 index 00000000..da201049 --- /dev/null +++ b/Tests/Mockolate.Internal.Tests/Setup/PropertySetupTests.cs @@ -0,0 +1,46 @@ +using Mockolate.Internal.Tests.TestHelpers; +using Mockolate.Setup; + +namespace Mockolate.Internal.Tests.Setup; + +public sealed class PropertySetupTests +{ + [Fact] + public async Task AutoInitializeWith_WhenAlreadyInitialized_ShouldNotOverwriteValue() + { + FakePropertySetup setup = new("p"); + IInteractivePropertySetup interactive = setup; + + interactive.InitializeWith(5); + interactive.InitializeWith(10); + + int value = interactive.InvokeGetter(null, MockBehavior.Default, () => -1); + await That(value).IsEqualTo(5); + } + + [Fact] + public async Task DefaultInvokeGetter_WhenRequestedTypeDiffersFromBackingType_ShouldFallBackToGenerator() + { + // 0x40400000 reinterpreted via Unsafe.As would yield 3.0f; the correct path + // must take the typeof-equality branch and fall through to the defaultValueGenerator. + PropertySetup.Default setup = new("p", 0x40400000); + IInteractivePropertySetup interactive = setup; + + float value = interactive.InvokeGetter(null, MockBehavior.Default, () => 99f); + + await That(value).IsEqualTo(99f); + } + + [Fact] + public async Task UserInitializeWith_SecondCall_ShouldNotOverwriteValue() + { + FakePropertySetup setup = new("p"); + IPropertySetup userFacing = setup; + + userFacing.InitializeWith(5); + userFacing.InitializeWith(10); + + int value = ((IInteractivePropertySetup)setup).InvokeGetter(null, MockBehavior.Default, () => -1); + await That(value).IsEqualTo(5); + } +} diff --git a/Tests/Mockolate.Internal.Tests/SetupMutationTests.cs b/Tests/Mockolate.Internal.Tests/SetupMutationTests.cs deleted file mode 100644 index 15f70b55..00000000 --- a/Tests/Mockolate.Internal.Tests/SetupMutationTests.cs +++ /dev/null @@ -1,171 +0,0 @@ -using System.Collections.Generic; -using Mockolate.Internal.Tests.TestHelpers; -using Mockolate.Setup; - -namespace Mockolate.Internal.Tests; - -public class SetupMutationTests -{ - public sealed class MethodSetupsFindTests - { - [Fact] - public async Task GetLatestOrDefault_WithSingleMatchingSetup_ShouldReturnIt() - { - MockSetups.MethodSetups setups = new(); - FakeMethodSetup setup = new(); - setups.Add(setup); - - MethodSetup? result = setups.GetLatestOrDefault(_ => true); - - await That(result).IsSameAs(setup); - } - } - - public sealed class PropertySetupsCountTests - { - [Fact] - public async Task Add_ReplacingDefaultWithUserSetup_ShouldIncrementCountByOne() - { - MockSetups.PropertySetups setups = new(); - PropertySetup defaultSetup = new PropertySetup.Default("p", 0); - FakePropertySetup userSetup = new("p"); - - setups.Add(defaultSetup); - await That(setups.Count).IsEqualTo(0); - - setups.Add(userSetup); - - await That(setups.Count).IsEqualTo(1); - setups.TryGetValue("p", out PropertySetup? found); - await That(found).IsSameAs(userSetup); - } - } - - public sealed class StatePassingInvokeTests - { - [Fact] - public async Task ShouldIncludeIndexWhenMatching() - { - bool wasInvoked = false; - List values = []; - Callback sut = new(() => { }); - sut.Only(2); - sut.When(v => v > 1); - - int index = 0; - for (int i = 0; i < 5; i++) - { - sut.Invoke(wasInvoked, ref index, values, static (v, _, list) => list.Add(v)); - } - - await That(values).IsEqualTo([2, 3,]); - } - - [Fact] - public async Task ShouldLimitExecutionWhenRunningInParallel() - { - bool wasInvoked = false; - List values = []; - Callback sut = new(() => { }); - sut.Only(2); - sut.InParallel(); - - int index = 0; - for (int i = 0; i < 5; i++) - { - sut.Invoke(wasInvoked, ref index, values, static (v, _, list) => list.Add(v)); - } - - await That(values).IsEqualTo([0, 1,]); - } - - [Theory] - [InlineData(2, 2, 0, 1, 1, 2, 2, 2, 2, 2, 2, 2)] - [InlineData(2, 3, 0, 1, 1, 2, 2, 3, 3, 3, 3, 3)] - public async Task ShouldIncrementIndexWheneverForIsExhausted( - int @for, int only, params int[] expectResult) - { - bool wasInvoked = false; - List indexValues = []; - Callback sut = new(() => { }); - sut.For(@for); - sut.Only(only); - - int index = 0; - for (int iteration = 1; iteration <= 10; iteration++) - { - sut.Invoke(wasInvoked, ref index, 0, static (_, _, _) => { }); - indexValues.Add(index); - } - - await That(indexValues).IsEqualTo(expectResult); - } - } - - public sealed class PropertySetupInitializeTests - { - [Fact] - public async Task AutoInitializeWith_WhenAlreadyInitialized_ShouldNotOverwriteValue() - { - FakePropertySetup setup = new("p"); - IInteractivePropertySetup interactive = setup; - - interactive.InitializeWith(5); - interactive.InitializeWith(10); - - int value = interactive.InvokeGetter(null, MockBehavior.Default, () => -1); - await That(value).IsEqualTo(5); - } - - [Fact] - public async Task UserInitializeWith_SecondCall_ShouldNotOverwriteValue() - { - FakePropertySetup setup = new("p"); - IPropertySetup userFacing = setup; - - userFacing.InitializeWith(5); - userFacing.InitializeWith(10); - - int value = ((IInteractivePropertySetup)setup).InvokeGetter(null, MockBehavior.Default, () => -1); - await That(value).IsEqualTo(5); - } - - [Fact] - public async Task DefaultInvokeGetter_WhenRequestedTypeDiffersFromBackingType_ShouldFallBackToGenerator() - { - // 0x40400000 reinterpreted via Unsafe.As would yield 3.0f; the correct path - // must take the typeof-equality branch and fall through to the defaultValueGenerator. - PropertySetup.Default setup = new("p", 0x40400000); - IInteractivePropertySetup interactive = setup; - - float value = interactive.InvokeGetter(null, MockBehavior.Default, () => 99f); - - await That(value).IsEqualTo(99f); - } - } - - public sealed class StatePassingReturnInvokeTests - { - [Fact] - public async Task ShouldIncludeIndexWhenMatching() - { - List values = []; - Callback sut = new(() => { }); - sut.Only(2); - sut.When(v => v > 1); - - int index = 0; - for (int i = 0; i < 5; i++) - { - sut.Invoke, string>(ref index, values, - static (v, _, list) => - { - list.Add(v); - return ""; - }, out _); - } - - await That(values).IsEqualTo([2, 3,]); - } - } -} diff --git a/Tests/Mockolate.Internal.Tests/TestHelpers/FakeIndexerAccess.cs b/Tests/Mockolate.Internal.Tests/TestHelpers/FakeIndexerAccess.cs index d28def08..ec7e0159 100644 --- a/Tests/Mockolate.Internal.Tests/TestHelpers/FakeIndexerAccess.cs +++ b/Tests/Mockolate.Internal.Tests/TestHelpers/FakeIndexerAccess.cs @@ -9,5 +9,6 @@ internal sealed class FakeIndexerAccess : IndexerAccess public override object? GetParameterValueAt(int index) => null; - protected override IndexerValueStorage? TraverseStorage(IndexerValueStorage? storage, bool createMissing) => Storage; + protected override IndexerValueStorage? TraverseStorage(IndexerValueStorage? storage, bool createMissing) + => Storage; } diff --git a/Tests/Mockolate.Internal.Tests/TestHelpers/FakeIndexerSetup.cs b/Tests/Mockolate.Internal.Tests/TestHelpers/FakeIndexerSetup.cs index 5a59439f..233bfefb 100644 --- a/Tests/Mockolate.Internal.Tests/TestHelpers/FakeIndexerSetup.cs +++ b/Tests/Mockolate.Internal.Tests/TestHelpers/FakeIndexerSetup.cs @@ -25,7 +25,8 @@ public override TResult GetResult(IndexerAccess access, MockBehavior be public override TResult GetResult(IndexerAccess access, MockBehavior behavior) => default!; - public override TResult GetResult(IndexerAccess access, MockBehavior behavior, Func defaultValueGenerator) + public override TResult GetResult(IndexerAccess access, MockBehavior behavior, + Func defaultValueGenerator) => defaultValueGenerator(); public override void SetResult(IndexerAccess access, MockBehavior behavior, TResult value) diff --git a/Tests/Mockolate.Internal.Tests/WebTests.cs b/Tests/Mockolate.Internal.Tests/Web/HttpClientExtensionsTests.cs similarity index 96% rename from Tests/Mockolate.Internal.Tests/WebTests.cs rename to Tests/Mockolate.Internal.Tests/Web/HttpClientExtensionsTests.cs index 57cdfe69..1bfea557 100644 --- a/Tests/Mockolate.Internal.Tests/WebTests.cs +++ b/Tests/Mockolate.Internal.Tests/Web/HttpClientExtensionsTests.cs @@ -4,9 +4,9 @@ using Mockolate.Parameters; using Mockolate.Web; -namespace Mockolate.Internal.Tests; +namespace Mockolate.Internal.Tests.Web; -public class WebTests +public class HttpClientExtensionsTests { [Fact] public async Task WhenParameterImplementsIHttpRequestMessagePropertyParameter_ShouldUseThisMatch() diff --git a/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithStringTests.cs b/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithStringTests.cs index 1a6cc196..9b70d895 100644 --- a/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithStringTests.cs +++ b/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithStringTests.cs @@ -95,7 +95,7 @@ void Act() } await That(Act).Throws() - .WithMessage("s => s.StartsWith(\"abc\") and s => s.EndsWith(\"xyz\")"); + .WithMessage("*s => s.StartsWith(\"abc\") and s => s.EndsWith(\"xyz\")*").AsWildcard(); } [Theory] From 08de127ca5d60587f644e398905a8951b3ef2874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Breu=C3=9F=20Valentin?= Date: Thu, 23 Apr 2026 08:23:29 +0200 Subject: [PATCH 13/13] Review issues --- .../Registry/MockRegistryTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/Mockolate.Internal.Tests/Registry/MockRegistryTests.cs b/Tests/Mockolate.Internal.Tests/Registry/MockRegistryTests.cs index 6ede380f..470f52c1 100644 --- a/Tests/Mockolate.Internal.Tests/Registry/MockRegistryTests.cs +++ b/Tests/Mockolate.Internal.Tests/Registry/MockRegistryTests.cs @@ -6,7 +6,7 @@ namespace Mockolate.Internal.Tests.Registry; public sealed class MockRegistryTests { - public sealed class GetIndexerSetupScenarioScoping + public sealed class GetIndexerSetupScenarioScopingTests { [Fact] public async Task WithActiveScenario_ShouldReturnScopedSetupOverGlobalSetup() @@ -38,7 +38,7 @@ public async Task WithoutActiveScenario_ShouldFallBackToGlobalSetup() } } - public sealed class IndexerFallbackStoresValue + public sealed class IndexerFallbackStoresValueTests { [Fact] public async Task ApplyIndexerGetter_WithNullSetup_ShouldStoreBaseValueForLaterLookup() @@ -71,7 +71,7 @@ public async Task GetIndexerFallback_ShouldStoreDefaultForLaterLookup() } } - public sealed class InitializeStorage + public sealed class InitializeStorageTests { [Fact] public async Task WithNegative_ShouldThrowWithDescriptiveMessage() @@ -101,7 +101,7 @@ void Act() } } - public sealed class WrapConstructor + public sealed class WrapConstructorTests { [Fact] public async Task WhenBehaviorSkipsInteractionRecording_WrappedRegistryAlsoSkipsRecording()