diff --git a/Mockolate.Migration.slnx b/Mockolate.Migration.slnx
index 4217b11..a945f8d 100644
--- a/Mockolate.Migration.slnx
+++ b/Mockolate.Migration.slnx
@@ -2,6 +2,10 @@
+
+
+
+
diff --git a/Pipeline/Build.UnitTest.cs b/Pipeline/Build.UnitTest.cs
index 9d51c7c..be24468 100644
--- a/Pipeline/Build.UnitTest.cs
+++ b/Pipeline/Build.UnitTest.cs
@@ -16,6 +16,8 @@ partial class Build
Project[] UnitTestProjects =>
[
+ Solution.Playground.Mockolate_Migration_MoqPlayground,
+ Solution.Playground.Mockolate_Migration_NSubstitutePlayground,
Solution.Tests.Mockolate_Migration_Tests,
];
diff --git a/Tests/Mockolate.Migration.MoqPlayground/ArgumentMatcherTests.cs b/Tests/Mockolate.Migration.MoqPlayground/ArgumentMatcherTests.cs
new file mode 100644
index 0000000..2b8c21a
--- /dev/null
+++ b/Tests/Mockolate.Migration.MoqPlayground/ArgumentMatcherTests.cs
@@ -0,0 +1,98 @@
+using System.Text.RegularExpressions;
+using Mockolate.Migration.MoqPlayground.Domain;
+using Moq;
+
+namespace Mockolate.Migration.MoqPlayground;
+
+using It = Moq.It;
+using Range = Moq.Range;
+
+/// Argument matchers: It.IsAny / It.Is / It.IsRegex / It.IsInRange / It.IsNotNull / It.Ref / out.
+public class ArgumentMatcherTests
+{
+ [Fact]
+ public async Task ItIs_predicate_matchesEvenAmounts()
+ {
+ Mock dispenser = new();
+ dispenser.Setup(d => d.Dispense("Dark", It.Is(i => i % 2 == 0))).Returns(true);
+
+ await That(dispenser.Object.Dispense("Dark", 4)).IsTrue();
+ await That(dispenser.Object.Dispense("Dark", 3)).IsFalse();
+ }
+
+ [Fact]
+ public async Task ItIsAny_matchesAnyValue()
+ {
+ Mock dispenser = new();
+ dispenser.Setup(d => d.Dispense(It.IsAny(), It.IsAny())).Returns(true);
+
+ await That(dispenser.Object.Dispense("Dark", 1)).IsTrue();
+ await That(dispenser.Object.Dispense("Milk", 99)).IsTrue();
+ }
+
+ [Fact]
+ public async Task ItIsInRange_inclusive_matchesBoundaries()
+ {
+ Mock dispenser = new();
+ dispenser.Setup(d => d.Dispense("Dark", It.IsInRange(1, 5, Range.Inclusive))).Returns(true);
+
+ await That(dispenser.Object.Dispense("Dark", 1)).IsTrue();
+ await That(dispenser.Object.Dispense("Dark", 5)).IsTrue();
+ await That(dispenser.Object.Dispense("Dark", 6)).IsFalse();
+ }
+
+ [Fact]
+ public async Task ItIsNotNull_rejectsNull()
+ {
+ Mock factory = new();
+ factory.Setup(f => f.RegisterRecipe(It.IsNotNull())).Returns(true);
+
+ await That(factory.Object.RegisterRecipe("Pralines")).IsTrue();
+ await That(factory.Object.RegisterRecipe(null!)).IsFalse();
+ }
+
+ [Fact]
+ public async Task ItIsRegex_matchesPattern()
+ {
+ Mock factory = new();
+ factory.Setup(f => f.RegisterRecipe(It.IsRegex("^Dark", RegexOptions.IgnoreCase))).Returns(true);
+
+ await That(factory.Object.RegisterRecipe("DarkTruffle")).IsTrue();
+ await That(factory.Object.RegisterRecipe("MilkTruffle")).IsFalse();
+ }
+
+ [Fact]
+ public async Task OutParameter_isSetByItIsOut()
+ {
+ Mock dispenser = new();
+ int reserved = 7;
+ dispenser.Setup(d => d.TryReserve("Dark", out reserved)).Returns(true);
+
+ bool ok = dispenser.Object.TryReserve("Dark", out int actual);
+
+ await That(ok).IsTrue();
+ await That(actual).IsEqualTo(7);
+ }
+
+ [Fact]
+ public async Task PlainValue_isUsedAsExactMatch()
+ {
+ Mock dispenser = new();
+ dispenser.Setup(d => d.Dispense("Milk", 3)).Returns(true);
+
+ await That(dispenser.Object.Dispense("Milk", 3)).IsTrue();
+ await That(dispenser.Object.Dispense("Milk", 4)).IsFalse();
+ }
+
+ [Fact]
+ public async Task RefParameter_anyMatch_acceptsAnyRef()
+ {
+ Mock dispenser = new();
+ dispenser.Setup(d => d.Refill("Dark", ref It.Ref.IsAny)).Returns(true);
+
+ int amount = 10;
+ bool ok = dispenser.Object.Refill("Dark", ref amount);
+
+ await That(ok).IsTrue();
+ }
+}
diff --git a/Tests/Mockolate.Migration.MoqPlayground/CreationTests.cs b/Tests/Mockolate.Migration.MoqPlayground/CreationTests.cs
new file mode 100644
index 0000000..5c083b9
--- /dev/null
+++ b/Tests/Mockolate.Migration.MoqPlayground/CreationTests.cs
@@ -0,0 +1,59 @@
+using Mockolate.Migration.MoqPlayground.Domain;
+using Moq;
+
+namespace Mockolate.Migration.MoqPlayground;
+
+using MockBehavior = Moq.MockBehavior;
+
+/// Mock construction patterns.
+public class CreationTests
+{
+ [Fact]
+ public async Task ClassMockWithConstructorArgs_isCreatedWithThoseArgs()
+ {
+ // ChocolateRecipe has a parameterless ctor, but Moq supports passing args to base.
+ Mock recipe = new();
+ recipe.SetupGet(r => r.Name).Returns("Praline");
+
+ await That(recipe.Object.Name).IsEqualTo("Praline");
+ }
+
+ [Fact]
+ public async Task DefaultLooseMock_returnsDefaultsForUnsetMembers()
+ {
+ Mock dispenser = new();
+
+ // Loose Moq returns default(bool) = false when not set up.
+ bool dispensed = dispenser.Object.Dispense("Dark", 1);
+
+ await That(dispensed).IsFalse();
+ }
+
+ [Fact]
+ public async Task ExplicitLooseMock_isEquivalentToDefault()
+ {
+ Mock dispenser = new(MockBehavior.Loose);
+
+ await That(dispenser.Object.Dispense("Dark", 1)).IsFalse();
+ }
+
+ [Fact]
+ public async Task ObjectAccess_isUsedToReachTheMockedInstance()
+ {
+ Mock factory = new();
+ factory.Setup(f => f.RegisterRecipe("Truffle")).Returns(true);
+
+ bool registered = factory.Object.RegisterRecipe("Truffle");
+
+ await That(registered).IsTrue();
+ }
+
+ [Fact]
+ public async Task StrictMock_throwsForUnsetMembers()
+ {
+ Mock dispenser = new(MockBehavior.Strict);
+
+ await That(() => dispenser.Object.Dispense("Dark", 1))
+ .Throws();
+ }
+}
diff --git a/Tests/Mockolate.Migration.MoqPlayground/Domain/ChocolateBar.cs b/Tests/Mockolate.Migration.MoqPlayground/Domain/ChocolateBar.cs
new file mode 100644
index 0000000..4300568
--- /dev/null
+++ b/Tests/Mockolate.Migration.MoqPlayground/Domain/ChocolateBar.cs
@@ -0,0 +1,4 @@
+namespace Mockolate.Migration.MoqPlayground.Domain;
+
+/// A baked chocolate bar.
+public sealed record ChocolateBar(string Type, int Cocoa, decimal Price);
diff --git a/Tests/Mockolate.Migration.MoqPlayground/Domain/ChocolateDispensedDelegate.cs b/Tests/Mockolate.Migration.MoqPlayground/Domain/ChocolateDispensedDelegate.cs
new file mode 100644
index 0000000..2a5db22
--- /dev/null
+++ b/Tests/Mockolate.Migration.MoqPlayground/Domain/ChocolateDispensedDelegate.cs
@@ -0,0 +1,4 @@
+namespace Mockolate.Migration.MoqPlayground.Domain;
+
+/// Custom event delegate carried by .
+public delegate void ChocolateDispensedDelegate(string type, int amount);
diff --git a/Tests/Mockolate.Migration.MoqPlayground/Domain/ChocolateRecipe.cs b/Tests/Mockolate.Migration.MoqPlayground/Domain/ChocolateRecipe.cs
new file mode 100644
index 0000000..fe566ae
--- /dev/null
+++ b/Tests/Mockolate.Migration.MoqPlayground/Domain/ChocolateRecipe.cs
@@ -0,0 +1,18 @@
+namespace Mockolate.Migration.MoqPlayground.Domain;
+
+/// Concrete recipe — used for partial mocks (Moq CallBase, NSubstitute ForPartsOf).
+public class ChocolateRecipe
+{
+ public virtual string Name { get; set; } = "Truffle";
+ public virtual int CocoaPercent { get; set; } = 70;
+
+ public virtual ChocolateBar Bake(int amount) =>
+ new(Name, CocoaPercent, amount * 1.5m);
+
+ public virtual bool Validate() => !string.IsNullOrEmpty(Name);
+
+ /// Used for Moq Protected().
+ protected virtual int InternalSecret() => 42;
+
+ public int CallInternalSecret() => InternalSecret();
+}
diff --git a/Tests/Mockolate.Migration.MoqPlayground/Domain/ChocolateShop.cs b/Tests/Mockolate.Migration.MoqPlayground/Domain/ChocolateShop.cs
new file mode 100644
index 0000000..feef5e1
--- /dev/null
+++ b/Tests/Mockolate.Migration.MoqPlayground/Domain/ChocolateShop.cs
@@ -0,0 +1,52 @@
+namespace Mockolate.Migration.MoqPlayground.Domain;
+
+/// The system-under-test that orchestrates the dependencies.
+public sealed class ChocolateShop
+{
+ private readonly IChocolateAuditor? _auditor;
+ private readonly IChocolateDispenser _dispenser;
+ private readonly IChocolateFactory _factory;
+
+ public ChocolateShop(
+ IChocolateDispenser dispenser,
+ IChocolateFactory factory,
+ IChocolateAuditor? auditor = null)
+ {
+ _dispenser = dispenser;
+ _factory = factory;
+ _auditor = auditor;
+ _dispenser.ChocolateDispensed += OnDispensed;
+ }
+
+ public int TotalSold { get; private set; }
+
+ public string DispenserName
+ {
+ get => _dispenser.Name;
+ set => _dispenser.Name = value;
+ }
+
+ public bool Sell(string type, int amount, decimal pricePerUnit = 1.5m)
+ {
+ if (!_dispenser.Dispense(type, amount))
+ {
+ return false;
+ }
+
+ _auditor?.RecordSale(type, amount, amount * pricePerUnit);
+ return true;
+ }
+
+ public Task SellAsync(string type, int amount)
+ => _dispenser.DispenseAsync(type, amount);
+
+ public Task RestockAsync(string recipe, int cocoa)
+ => _factory.BakeAsync(recipe, cocoa);
+
+ public int CheckStock(string type) => _dispenser[type];
+
+ public bool TryReserveStock(string type, out int reserved)
+ => _dispenser.TryReserve(type, out reserved);
+
+ private void OnDispensed(string type, int amount) => TotalSold += amount;
+}
diff --git a/Tests/Mockolate.Migration.MoqPlayground/Domain/IChocolateAuditor.cs b/Tests/Mockolate.Migration.MoqPlayground/Domain/IChocolateAuditor.cs
new file mode 100644
index 0000000..348bbde
--- /dev/null
+++ b/Tests/Mockolate.Migration.MoqPlayground/Domain/IChocolateAuditor.cs
@@ -0,0 +1,8 @@
+namespace Mockolate.Migration.MoqPlayground.Domain;
+
+/// Used for multi-interface mocks (Moq As<T>(), NSubstitute Substitute.For<T1,T2>()).
+public interface IChocolateAuditor
+{
+ int AuditCount { get; }
+ void RecordSale(string type, int amount, decimal total);
+}
diff --git a/Tests/Mockolate.Migration.MoqPlayground/Domain/IChocolateDispenser.cs b/Tests/Mockolate.Migration.MoqPlayground/Domain/IChocolateDispenser.cs
new file mode 100644
index 0000000..98a7d69
--- /dev/null
+++ b/Tests/Mockolate.Migration.MoqPlayground/Domain/IChocolateDispenser.cs
@@ -0,0 +1,27 @@
+namespace Mockolate.Migration.MoqPlayground.Domain;
+
+///
+/// Surface intended to exercise as much of Moq/NSubstitute as possible:
+/// indexer, properties (read/write), method overloads, async, ref, out, custom event, standard event.
+///
+public interface IChocolateDispenser
+{
+ int this[string type] { get; set; }
+ int TotalDispensed { get; set; }
+ string Name { get; set; }
+
+ bool Dispense(string type, int amount);
+ bool Dispense(string type);
+ Task DispenseAsync(string type, int amount);
+
+ /// Tries to reserve some stock for the given type, returning the reserved amount.
+ bool TryReserve(string type, out int reserved);
+
+ /// Refills stock; the caller passes a desired amount and gets the actual amount back via ref.
+ bool Refill(string type, ref int amount);
+
+ int CountByType(string type);
+
+ event ChocolateDispensedDelegate ChocolateDispensed;
+ event EventHandler StockLow;
+}
diff --git a/Tests/Mockolate.Migration.MoqPlayground/Domain/IChocolateFactory.cs b/Tests/Mockolate.Migration.MoqPlayground/Domain/IChocolateFactory.cs
new file mode 100644
index 0000000..7e58cc5
--- /dev/null
+++ b/Tests/Mockolate.Migration.MoqPlayground/Domain/IChocolateFactory.cs
@@ -0,0 +1,10 @@
+namespace Mockolate.Migration.MoqPlayground.Domain;
+
+/// Used to exercise async, generics on parameters, collection parameters.
+public interface IChocolateFactory
+{
+ int Capacity { get; }
+ Task BakeAsync(string recipe, int cocoa);
+ Task> BatchBakeAsync(IEnumerable recipes);
+ bool RegisterRecipe(string name);
+}
diff --git a/Tests/Mockolate.Migration.MoqPlayground/Domain/InvalidChocolateException.cs b/Tests/Mockolate.Migration.MoqPlayground/Domain/InvalidChocolateException.cs
new file mode 100644
index 0000000..804e295
--- /dev/null
+++ b/Tests/Mockolate.Migration.MoqPlayground/Domain/InvalidChocolateException.cs
@@ -0,0 +1,8 @@
+namespace Mockolate.Migration.MoqPlayground.Domain;
+
+/// Domain exception thrown for invalid chocolate operations.
+public class InvalidChocolateException : Exception
+{
+ public InvalidChocolateException() { }
+ public InvalidChocolateException(string message) : base(message) { }
+}
diff --git a/Tests/Mockolate.Migration.MoqPlayground/EventTests.cs b/Tests/Mockolate.Migration.MoqPlayground/EventTests.cs
new file mode 100644
index 0000000..5a35051
--- /dev/null
+++ b/Tests/Mockolate.Migration.MoqPlayground/EventTests.cs
@@ -0,0 +1,80 @@
+using Mockolate.Migration.MoqPlayground.Domain;
+using Moq;
+
+namespace Mockolate.Migration.MoqPlayground;
+
+using It = Moq.It;
+
+/// Event subscription / raising / verification.
+public class EventTests
+{
+ [Fact]
+ public async Task Raise_customDelegate_invokesSubscribedHandler()
+ {
+ Mock dispenser = new();
+ string? observedType = null;
+ int observedAmount = 0;
+ dispenser.Object.ChocolateDispensed += (t, a) =>
+ {
+ observedType = t;
+ observedAmount = a;
+ };
+
+ dispenser.Raise(d => d.ChocolateDispensed += null, "Dark", 5);
+
+ await That(observedType).IsEqualTo("Dark");
+ await That(observedAmount).IsEqualTo(5);
+ }
+
+ [Fact]
+ public async Task Raise_eventHandlerStandard_passesArgs()
+ {
+ Mock dispenser = new();
+ int? observed = null;
+ dispenser.Object.StockLow += (_, low) => observed = low;
+
+ dispenser.Raise(d => d.StockLow += null, dispenser.Object, 2);
+
+ await That(observed).IsEqualTo(2);
+ }
+
+ [Fact]
+ public async Task ShopSubscribesOnConstruction_andTracksDispensedAmounts()
+ {
+ Mock dispenser = new();
+ Mock factory = new();
+ dispenser.Setup(d => d.Dispense(It.IsAny(), It.IsAny())).Returns(true);
+ ChocolateShop shop = new(dispenser.Object, factory.Object);
+
+ shop.Sell("Dark", 2);
+ dispenser.Raise(d => d.ChocolateDispensed += null, "Dark", 2);
+ shop.Sell("Milk", 5);
+ dispenser.Raise(d => d.ChocolateDispensed += null, "Milk", 5);
+
+ await That(shop.TotalSold).IsEqualTo(7);
+ dispenser.VerifyAdd(
+ d => d.ChocolateDispensed += It.IsAny(),
+ Times.Once());
+ }
+
+ [Fact]
+ public async Task VerifyAdd_recordsSubscription()
+ {
+ Mock dispenser = new();
+ ChocolateDispensedDelegate handler = (_, _) => { };
+ dispenser.Object.ChocolateDispensed += handler;
+
+ dispenser.VerifyAdd(d => d.ChocolateDispensed += It.IsAny(), Times.Once());
+ }
+
+ [Fact]
+ public async Task VerifyRemove_recordsUnsubscription()
+ {
+ Mock dispenser = new();
+ ChocolateDispensedDelegate handler = (_, _) => { };
+ dispenser.Object.ChocolateDispensed += handler;
+ dispenser.Object.ChocolateDispensed -= handler;
+
+ dispenser.VerifyRemove(d => d.ChocolateDispensed -= It.IsAny(), Times.Once());
+ }
+}
diff --git a/Tests/Mockolate.Migration.MoqPlayground/Mockolate.Migration.MoqPlayground.csproj b/Tests/Mockolate.Migration.MoqPlayground/Mockolate.Migration.MoqPlayground.csproj
new file mode 100644
index 0000000..f550433
--- /dev/null
+++ b/Tests/Mockolate.Migration.MoqPlayground/Mockolate.Migration.MoqPlayground.csproj
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tests/Mockolate.Migration.MoqPlayground/SequenceTests.cs b/Tests/Mockolate.Migration.MoqPlayground/SequenceTests.cs
new file mode 100644
index 0000000..2c9c0ef
--- /dev/null
+++ b/Tests/Mockolate.Migration.MoqPlayground/SequenceTests.cs
@@ -0,0 +1,41 @@
+using Mockolate.Migration.MoqPlayground.Domain;
+using Moq;
+
+namespace Mockolate.Migration.MoqPlayground;
+
+using It = Moq.It;
+using MockBehavior = Moq.MockBehavior;
+
+/// MockSequence + InSequence call ordering (Moq pattern).
+public class SequenceTests
+{
+ [Fact]
+ public async Task MockSequence_strictOrdering_isHonored()
+ {
+ Mock dispenser = new(MockBehavior.Strict);
+ MockSequence sequence = new();
+
+ dispenser.InSequence(sequence).Setup(d => d.Dispense("Dark", 1)).Returns(true);
+ dispenser.InSequence(sequence).Setup(d => d.Dispense("Milk", 2)).Returns(true);
+ dispenser.InSequence(sequence).Setup(d => d.Dispense("White", 3)).Returns(true);
+
+ await That(dispenser.Object.Dispense("Dark", 1)).IsTrue();
+ await That(dispenser.Object.Dispense("Milk", 2)).IsTrue();
+ await That(dispenser.Object.Dispense("White", 3)).IsTrue();
+ }
+
+ [Fact]
+ public async Task SetupSequence_returnsValuesInOrder()
+ {
+ Mock factory = new();
+ factory.SetupSequence(f => f.RegisterRecipe(It.IsAny()))
+ .Returns(true)
+ .Returns(false)
+ .Throws(new InvalidChocolateException("registry full"));
+
+ await That(factory.Object.RegisterRecipe("a")).IsTrue();
+ await That(factory.Object.RegisterRecipe("b")).IsFalse();
+ await That(() => factory.Object.RegisterRecipe("c"))
+ .Throws();
+ }
+}
diff --git a/Tests/Mockolate.Migration.MoqPlayground/SetupTests.cs b/Tests/Mockolate.Migration.MoqPlayground/SetupTests.cs
new file mode 100644
index 0000000..2a3c238
--- /dev/null
+++ b/Tests/Mockolate.Migration.MoqPlayground/SetupTests.cs
@@ -0,0 +1,192 @@
+using Mockolate.Migration.MoqPlayground.Domain;
+using Moq;
+
+namespace Mockolate.Migration.MoqPlayground;
+
+using It = Moq.It;
+
+/// Method/property setup patterns: Returns / Throws / Callback / Sequence / Async.
+public class SetupTests
+{
+ [Fact]
+ public async Task Callback_observesArgumentsBeforeReturning()
+ {
+ Mock dispenser = new();
+ string? observedType = null;
+ int observedAmount = 0;
+ dispenser
+ .Setup(d => d.Dispense(It.IsAny(), It.IsAny()))
+ .Callback((t, a) =>
+ {
+ observedType = t;
+ observedAmount = a;
+ })
+ .Returns(true);
+
+ dispenser.Object.Dispense("Milk", 4);
+
+ await That(observedType).IsEqualTo("Milk");
+ await That(observedAmount).IsEqualTo(4);
+ }
+
+ [Fact]
+ public async Task NestedMockSetup_recursive_chainsThroughChildMock()
+ {
+ // Auto-mocking hierarchy: Moq creates child mocks for properties on demand.
+ Mock outer = new()
+ {
+ DefaultValue = DefaultValue.Mock,
+ };
+ outer.Setup(o => o.Inner.Inner.Name).Returns("deep");
+
+ await That(outer.Object.Inner.Inner.Name).IsEqualTo("deep");
+ }
+
+ [Fact]
+ public async Task Returns_argumentBased_evaluatesFromArgument()
+ {
+ Mock dispenser = new();
+ dispenser.Setup(d => d.Dispense(It.IsAny())).Returns((string s) => s.Length > 0);
+
+ await That(dispenser.Object.Dispense("Dark")).IsTrue();
+ await That(dispenser.Object.Dispense("")).IsFalse();
+ }
+
+ [Fact]
+ public async Task Returns_directValue_dispensesAndShopRecordsTotal()
+ {
+ Mock dispenser = new();
+ Mock factory = new();
+ dispenser.Setup(d => d.Dispense("Dark", 3)).Returns(true);
+ ChocolateShop shop = new(dispenser.Object, factory.Object);
+
+ bool sold = shop.Sell("Dark", 3);
+ dispenser.Raise(d => d.ChocolateDispensed += null, "Dark", 3);
+
+ await That(sold).IsTrue();
+ await That(shop.TotalSold).IsEqualTo(3);
+ }
+
+ [Fact]
+ public async Task Returns_lazyFactory_evaluatedOnEachCall()
+ {
+ Mock dispenser = new();
+ int count = 1;
+ dispenser.Setup(d => d.CountByType("Dark")).Returns(() => count);
+
+ int first = dispenser.Object.CountByType("Dark");
+ count = 5;
+ int second = dispenser.Object.CountByType("Dark");
+
+ await That(first).IsEqualTo(1);
+ await That(second).IsEqualTo(5);
+ }
+
+ [Fact]
+ public async Task ReturnsAsync_completesWithValue()
+ {
+ Mock dispenser = new();
+ dispenser.Setup(d => d.DispenseAsync("Dark", 1)).ReturnsAsync(true);
+
+ await That(await dispenser.Object.DispenseAsync("Dark", 1)).IsTrue();
+ }
+
+ [Fact]
+ public async Task ReturnsAsync_factory_evaluatesPerCall()
+ {
+ Mock factory = new();
+ factory.Setup(f => f.BakeAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync((string r, int c) => new ChocolateBar(r, c, c * 0.05m));
+
+ ChocolateBar bar = await factory.Object.BakeAsync("Dark", 80);
+
+ await That(bar.Type).IsEqualTo("Dark");
+ await That(bar.Cocoa).IsEqualTo(80);
+ await That(bar.Price).IsEqualTo(4.0m);
+ }
+
+ [Fact]
+ public async Task SetupGet_property_returnsConfiguredValue()
+ {
+ Mock dispenser = new();
+ dispenser.SetupGet(d => d.Name).Returns("Choco-9000");
+
+ await That(dispenser.Object.Name).IsEqualTo("Choco-9000");
+ }
+
+ [Fact]
+ public async Task SetupProperty_tracksAssignmentsAndReturnsLastValue()
+ {
+ Mock dispenser = new();
+ dispenser.SetupProperty(d => d.Name);
+
+ dispenser.Object.Name = "ChocoMatic";
+
+ await That(dispenser.Object.Name).IsEqualTo("ChocoMatic");
+ }
+
+ [Fact]
+ public async Task SetupProperty_withInitialValue_returnsThatBeforeAssignment()
+ {
+ Mock dispenser = new();
+ dispenser.SetupProperty(d => d.Name, "Default");
+
+ await That(dispenser.Object.Name).IsEqualTo("Default");
+ }
+
+ [Fact]
+ public async Task SetupSequence_returnsValuesInOrder_thenDefault()
+ {
+ Mock dispenser = new();
+ dispenser.SetupSequence(d => d.Dispense(It.IsAny(), It.IsAny()))
+ .Returns(true)
+ .Throws(new InvalidChocolateException("temporarily out"))
+ .Returns(false);
+
+ await That(dispenser.Object.Dispense("Dark", 1)).IsTrue();
+ await That(() => dispenser.Object.Dispense("Dark", 1))
+ .Throws();
+ await That(dispenser.Object.Dispense("Dark", 1)).IsFalse();
+ }
+
+ [Fact]
+ public async Task Throws_genericException_isRaisedOnInvocation()
+ {
+ Mock dispenser = new();
+ dispenser.Setup(d => d.Dispense("reset", 0)).Throws();
+
+ await That(() => dispenser.Object.Dispense("reset", 0))
+ .Throws();
+ }
+
+ [Fact]
+ public async Task Throws_specificInstance_isRaisedOnInvocation()
+ {
+ Mock dispenser = new();
+ dispenser.Setup(d => d.Dispense("", 0)).Throws(new InvalidChocolateException("empty type"));
+
+ await That(() => dispenser.Object.Dispense("", 0))
+ .Throws()
+ .WithMessage("empty type");
+ }
+
+ [Fact]
+ public async Task ThrowsAsync_completesWithException()
+ {
+ Mock dispenser = new();
+ dispenser.Setup(d => d.DispenseAsync("Dark", 1)).ThrowsAsync(new TimeoutException());
+
+ Task Act()
+ {
+ return dispenser.Object.DispenseAsync("Dark", 1);
+ }
+
+ await That((Func>)Act).Throws();
+ }
+
+ public interface INested
+ {
+ INested Inner { get; }
+ string Name { get; }
+ }
+}
diff --git a/Tests/Mockolate.Migration.MoqPlayground/UnsupportedFeatureTests.cs b/Tests/Mockolate.Migration.MoqPlayground/UnsupportedFeatureTests.cs
new file mode 100644
index 0000000..83590fa
--- /dev/null
+++ b/Tests/Mockolate.Migration.MoqPlayground/UnsupportedFeatureTests.cs
@@ -0,0 +1,176 @@
+using Mockolate.Migration.MoqPlayground.Domain;
+using Moq;
+using Moq.Protected;
+
+namespace Mockolate.Migration.MoqPlayground;
+
+using It = Moq.It;
+using MockBehavior = Moq.MockBehavior;
+
+///
+/// Moq features the migration does NOT yet handle. They still pass against Moq;
+/// run the migration to see what is/isn't transformed and where manual rewrites are needed.
+///
+public class UnsupportedFeatureTests
+{
+ // NOT YET MIGRATED: CallBase = true (delegate to base implementation)
+ [Fact]
+ public async Task CallBase_invokesBaseClassImplementation()
+ {
+ Mock recipe = new()
+ {
+ CallBase = true,
+ };
+
+ // Validate() falls through to base, which checks Name is non-empty
+ bool ok = recipe.Object.Validate();
+
+ await That(ok).IsTrue();
+ }
+
+ // NOT YET MIGRATED: Custom DefaultValueProvider
+ [Fact]
+ public async Task DefaultValueProviderMock_returnsAutoMockedReferenceTypes()
+ {
+ Mock factory = new()
+ {
+ DefaultValue = DefaultValue.Mock,
+ };
+
+ // Calling an unconfigured Task-returning member yields a Task that completes
+ // (with an auto-mocked default), rather than null/NullReferenceException.
+ IReadOnlyList bars = await factory.Object.BatchBakeAsync(["auto"]);
+
+ await That(bars).IsNotNull();
+ }
+
+ // NOT YET MIGRATED: It.IsNotIn / It.IsIn (set membership matchers)
+ [Fact]
+ public async Task ItIsIn_acceptsAnyValueFromTheSet()
+ {
+ Mock dispenser = new();
+ dispenser.Setup(d => d.Dispense(It.IsIn("Dark", "Milk"), 1)).Returns(true);
+
+ await That(dispenser.Object.Dispense("Dark", 1)).IsTrue();
+ await That(dispenser.Object.Dispense("Milk", 1)).IsTrue();
+ await That(dispenser.Object.Dispense("White", 1)).IsFalse();
+ }
+
+ // NOT YET MIGRATED: Mock.As() to add a secondary interface
+ [Fact]
+ public async Task MockAs_castToAdditionalInterface()
+ {
+ Mock dispenser = new();
+ dispenser.As()
+ .Setup(a => a.AuditCount).Returns(7);
+
+ IChocolateAuditor auditor = (IChocolateAuditor)dispenser.Object;
+ await That(auditor.AuditCount).IsEqualTo(7);
+ }
+
+ // NOT YET MIGRATED: Mock.Of() (LINQ to Mocks)
+ [Fact]
+ public async Task MockOf_setsUpAllReturnsImplicitly()
+ {
+ IChocolateDispenser dispenser = Moq.Mock.Of(d =>
+ d.Name == "Quick" &&
+ d.TotalDispensed == 99);
+
+ await That(dispenser.Name).IsEqualTo("Quick");
+ await That(dispenser.TotalDispensed).IsEqualTo(99);
+ }
+
+ // NOT YET MIGRATED: MockRepository for grouped Verifiable + VerifyAll
+ [Fact]
+ public async Task MockRepository_groupsAndVerifiesAllInOneShot()
+ {
+ MockRepository repo = new(MockBehavior.Strict);
+ Mock dispenser = repo.Create();
+ Mock factory = repo.Create();
+ dispenser.Setup(d => d.Dispense("Dark", 1)).Returns(true);
+ factory.Setup(f => f.RegisterRecipe("Truffle")).Returns(true);
+
+ _ = dispenser.Object.Dispense("Dark", 1);
+ _ = factory.Object.RegisterRecipe("Truffle");
+
+ repo.VerifyAll();
+ }
+
+ // NOT YET MIGRATED: Protected() to set up protected virtual members
+ [Fact]
+ public async Task Protected_setupOfProtectedMethod()
+ {
+ Mock recipe = new();
+ recipe.Protected().Setup("InternalSecret").Returns(123);
+
+ int secret = recipe.Object.CallInternalSecret();
+
+ await That(secret).IsEqualTo(123);
+ }
+
+ // NOT YET MIGRATED: Returns overload that takes the Mock itself (mock.Object self-reference)
+ [Fact]
+ public async Task Returns_factoryWithCapturedMock_canReadOtherSetups()
+ {
+ Mock dispenser = new();
+ dispenser.SetupGet(d => d.TotalDispensed).Returns(42);
+ dispenser
+ .Setup(d => d.CountByType(It.IsAny()))
+ .Returns(() => dispenser.Object.TotalDispensed / 2);
+
+ await That(dispenser.Object.CountByType("Dark")).IsEqualTo(21);
+ }
+
+ // NOT YET MIGRATED: SetupAllProperties stubs every readable+writable property
+ [Fact]
+ public async Task SetupAllProperties_makesAllPropertiesStateful()
+ {
+ Mock dispenser = new();
+ dispenser.SetupAllProperties();
+
+ dispenser.Object.Name = "All-Stub";
+ dispenser.Object.TotalDispensed = 12;
+
+ await That(dispenser.Object.Name).IsEqualTo("All-Stub");
+ await That(dispenser.Object.TotalDispensed).IsEqualTo(12);
+ }
+
+ // NOT YET MIGRATED: Strict mock with Verifiable() chain to check that every verifiable setup ran
+ [Fact]
+ public async Task Strict_withVerifiableSetups_passesWhenAllAreInvoked()
+ {
+ Mock dispenser = new(MockBehavior.Strict);
+ dispenser.Setup(d => d.Dispense("Dark", 1)).Returns(true).Verifiable();
+ dispenser.Setup(d => d.Dispense("Milk", 1)).Returns(true).Verifiable();
+
+ _ = dispenser.Object.Dispense("Dark", 1);
+ _ = dispenser.Object.Dispense("Milk", 1);
+
+ dispenser.Verify();
+ }
+
+ // NOT YET MIGRATED: Verifiable() + mock.Verify()
+ [Fact]
+ public async Task VerifiableSetup_andMockVerify()
+ {
+ Mock dispenser = new();
+ dispenser.Setup(d => d.Dispense("Dark", 1)).Returns(true).Verifiable();
+
+ _ = dispenser.Object.Dispense("Dark", 1);
+
+ dispenser.Verify();
+ }
+
+ // NOT YET MIGRATED: VerifyAll() / VerifyNoOtherCalls()
+ [Fact]
+ public async Task VerifyAll_andVerifyNoOtherCalls()
+ {
+ Mock dispenser = new();
+ dispenser.Setup(d => d.Dispense("Dark", 1)).Returns(true);
+
+ _ = dispenser.Object.Dispense("Dark", 1);
+
+ dispenser.VerifyAll();
+ dispenser.VerifyNoOtherCalls();
+ }
+}
diff --git a/Tests/Mockolate.Migration.MoqPlayground/Usings.cs b/Tests/Mockolate.Migration.MoqPlayground/Usings.cs
new file mode 100644
index 0000000..88f147d
--- /dev/null
+++ b/Tests/Mockolate.Migration.MoqPlayground/Usings.cs
@@ -0,0 +1,7 @@
+global using System;
+global using System.Collections.Generic;
+global using System.Threading;
+global using System.Threading.Tasks;
+global using Xunit;
+global using aweXpect;
+global using static aweXpect.Expect;
diff --git a/Tests/Mockolate.Migration.MoqPlayground/VerifyTests.cs b/Tests/Mockolate.Migration.MoqPlayground/VerifyTests.cs
new file mode 100644
index 0000000..0c46dce
--- /dev/null
+++ b/Tests/Mockolate.Migration.MoqPlayground/VerifyTests.cs
@@ -0,0 +1,124 @@
+using Mockolate.Migration.MoqPlayground.Domain;
+using Moq;
+
+namespace Mockolate.Migration.MoqPlayground;
+
+using It = Moq.It;
+using Range = Moq.Range;
+
+/// Verification patterns: Verify, VerifyGet, VerifySet, Times.*.
+public class VerifyTests
+{
+ [Fact]
+ public void Verify_method_atLeast()
+ {
+ Mock dispenser = new();
+ dispenser.Setup(d => d.Dispense(It.IsAny(), It.IsAny())).Returns(true);
+
+ dispenser.Object.Dispense("Dark", 1);
+ dispenser.Object.Dispense("Dark", 2);
+
+ dispenser.Verify(d => d.Dispense("Dark", It.IsAny()), Times.AtLeast(2));
+ dispenser.Verify(d => d.Dispense("Dark", It.IsAny()), Times.AtLeastOnce);
+ }
+
+ [Fact]
+ public void Verify_method_atMost()
+ {
+ Mock dispenser = new();
+ dispenser.Setup(d => d.Dispense(It.IsAny(), It.IsAny())).Returns(true);
+
+ dispenser.Object.Dispense("Dark", 1);
+
+ dispenser.Verify(d => d.Dispense("Dark", It.IsAny()), Times.AtMost(2));
+ dispenser.Verify(d => d.Dispense("Dark", It.IsAny()), Times.AtMostOnce);
+ }
+
+ [Fact]
+ public void Verify_method_between()
+ {
+ Mock dispenser = new();
+ dispenser.Setup(d => d.Dispense(It.IsAny(), It.IsAny())).Returns(true);
+
+ dispenser.Object.Dispense("Dark", 1);
+ dispenser.Object.Dispense("Dark", 2);
+
+ dispenser.Verify(
+ d => d.Dispense("Dark", It.IsAny()),
+ Times.Between(1, 3, Range.Inclusive));
+ }
+
+ [Fact]
+ public void Verify_method_exactCount()
+ {
+ Mock dispenser = new();
+ dispenser.Setup(d => d.Dispense(It.IsAny(), It.IsAny())).Returns(true);
+
+ dispenser.Object.Dispense("Dark", 1);
+ dispenser.Object.Dispense("Dark", 2);
+ dispenser.Object.Dispense("Dark", 3);
+
+ dispenser.Verify(d => d.Dispense("Dark", It.IsAny()), Times.Exactly(3));
+ }
+
+ [Fact]
+ public void Verify_method_wasCalledOnce()
+ {
+ Mock dispenser = new();
+ Mock factory = new();
+ dispenser.Setup(d => d.Dispense("Dark", 2)).Returns(true);
+ ChocolateShop shop = new(dispenser.Object, factory.Object);
+
+ shop.Sell("Dark", 2);
+
+ dispenser.Verify(d => d.Dispense("Dark", 2), Times.Once());
+ }
+
+ [Fact]
+ public void Verify_method_wasNeverCalled()
+ {
+ Mock dispenser = new();
+ Mock factory = new();
+ dispenser.Setup(d => d.Dispense("Dark", 1)).Returns(false);
+ ChocolateShop shop = new(dispenser.Object, factory.Object);
+
+ shop.Sell("Dark", 1); // dispense returns false → no audit follow-up
+
+ dispenser.Verify(d => d.Dispense("Milk", It.IsAny()), Times.Never);
+ }
+
+ [Fact]
+ public void VerifyGet_property_wasRead()
+ {
+ Mock dispenser = new();
+ dispenser.SetupGet(d => d.Name).Returns("Choc-Box");
+
+ _ = dispenser.Object.Name;
+ _ = dispenser.Object.Name;
+
+ dispenser.VerifyGet(d => d.Name, Times.Exactly(2));
+ }
+
+ [Fact]
+ public void VerifySet_anyValue_isMatchedWithItIsAny()
+ {
+ Mock dispenser = new();
+ dispenser.SetupProperty(d => d.TotalDispensed);
+
+ dispenser.Object.TotalDispensed = 7;
+ dispenser.Object.TotalDispensed = 9;
+
+ dispenser.VerifySet(d => d.TotalDispensed = It.IsAny(), Times.Exactly(2));
+ }
+
+ [Fact]
+ public void VerifySet_property_wasAssigned()
+ {
+ Mock dispenser = new();
+ dispenser.SetupProperty(d => d.Name);
+
+ dispenser.Object.Name = "Choco-2025";
+
+ dispenser.VerifySet(d => d.Name = "Choco-2025", Times.Once());
+ }
+}
diff --git a/Tests/Mockolate.Migration.NSubstitutePlayground/ArgumentMatcherTests.cs b/Tests/Mockolate.Migration.NSubstitutePlayground/ArgumentMatcherTests.cs
new file mode 100644
index 0000000..175ab64
--- /dev/null
+++ b/Tests/Mockolate.Migration.NSubstitutePlayground/ArgumentMatcherTests.cs
@@ -0,0 +1,58 @@
+using Mockolate.Migration.NSubstitutePlayground.Domain;
+using NSubstitute;
+
+namespace Mockolate.Migration.NSubstitutePlayground;
+
+/// Argument matchers: Arg.Any / Arg.Is / Arg.Compat. (Arg.Do/Arg.Invoke are in the Unsupported file.)
+public class ArgumentMatcherTests
+{
+ [Fact]
+ public async Task ArgAny_matchesAnyValue()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ dispenser.Dispense(Arg.Any(), Arg.Any()).Returns(true);
+
+ await That(dispenser.Dispense("Dark", 1)).IsTrue();
+ await That(dispenser.Dispense("Milk", 99)).IsTrue();
+ }
+
+ [Fact]
+ public async Task ArgCompat_AnyAndIs_workLikePlainArg()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ dispenser.Dispense(Arg.Compat.Any(), Arg.Compat.Is(i => i > 0)).Returns(true);
+
+ await That(dispenser.Dispense("Dark", 5)).IsTrue();
+ await That(dispenser.Dispense("Dark", 0)).IsFalse();
+ }
+
+ [Fact]
+ public async Task ArgIs_predicate_matchesEvenAmounts()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ dispenser.Dispense("Dark", Arg.Is(i => i % 2 == 0)).Returns(true);
+
+ await That(dispenser.Dispense("Dark", 4)).IsTrue();
+ await That(dispenser.Dispense("Dark", 3)).IsFalse();
+ }
+
+ [Fact]
+ public async Task ArgIs_value_matchesExactValue()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ dispenser.Dispense(Arg.Is("Dark"), Arg.Is(2)).Returns(true);
+
+ await That(dispenser.Dispense("Dark", 2)).IsTrue();
+ await That(dispenser.Dispense("Dark", 3)).IsFalse();
+ }
+
+ [Fact]
+ public async Task PlainValue_isUsedAsExactMatch()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ dispenser.Dispense("Milk", 3).Returns(true);
+
+ await That(dispenser.Dispense("Milk", 3)).IsTrue();
+ await That(dispenser.Dispense("Milk", 4)).IsFalse();
+ }
+}
diff --git a/Tests/Mockolate.Migration.NSubstitutePlayground/CreationTests.cs b/Tests/Mockolate.Migration.NSubstitutePlayground/CreationTests.cs
new file mode 100644
index 0000000..ed3aa17
--- /dev/null
+++ b/Tests/Mockolate.Migration.NSubstitutePlayground/CreationTests.cs
@@ -0,0 +1,60 @@
+using Mockolate.Migration.NSubstitutePlayground.Domain;
+using NSubstitute;
+
+namespace Mockolate.Migration.NSubstitutePlayground;
+
+/// Substitute creation patterns.
+public class CreationTests
+{
+ [Fact]
+ public async Task SubstituteFor_class_canConfigureVirtualMemberReturnValue()
+ {
+ // Configure a class substitute's virtual member to return a specific value.
+ ChocolateRecipe recipe = Substitute.For();
+ recipe.Name.Returns("Pralines");
+
+ await That(recipe.Name).IsEqualTo("Pralines");
+ }
+
+ [Fact]
+ public async Task SubstituteFor_multipleInterfaces_implementsBoth()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+
+ // The same proxy implements both, so a cast yields the secondary face.
+ IChocolateAuditor auditor = (IChocolateAuditor)dispenser;
+ auditor.RecordSale("Dark", 2, 3.0m);
+
+ await That(auditor.AuditCount).IsEqualTo(0);
+ }
+
+ [Fact]
+ public async Task SubstituteFor_singleInterface_createsLooseSubstitute()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+
+ // Loose substitute returns default (false) for unconfigured calls.
+ await That(dispenser.Dispense("Dark", 1)).IsFalse();
+ }
+
+ [Fact]
+ public async Task SubstituteForPartsOf_callsRealVirtualMembers()
+ {
+ ChocolateRecipe recipe = Substitute.ForPartsOf();
+
+ // Virtual members fall through to the real implementation unless configured.
+ await That(recipe.Validate()).IsTrue();
+ await That(recipe.Name).IsEqualTo("Truffle");
+ }
+
+ [Fact]
+ public async Task SubstituteForTypeForwardingTo_forwardsCallsToConcreteImpl()
+ {
+ IChocolateAuditor auditor = Substitute.ForTypeForwardingTo();
+
+ auditor.RecordSale("Dark", 1, 1.5m);
+
+ // The forwarded StaticAuditor increments its counter.
+ await That(auditor.AuditCount).IsEqualTo(1);
+ }
+}
diff --git a/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/ChocolateBar.cs b/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/ChocolateBar.cs
new file mode 100644
index 0000000..1e69f5a
--- /dev/null
+++ b/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/ChocolateBar.cs
@@ -0,0 +1,4 @@
+namespace Mockolate.Migration.NSubstitutePlayground.Domain;
+
+/// A baked chocolate bar.
+public sealed record ChocolateBar(string Type, int Cocoa, decimal Price);
diff --git a/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/ChocolateDispensedDelegate.cs b/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/ChocolateDispensedDelegate.cs
new file mode 100644
index 0000000..b47d407
--- /dev/null
+++ b/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/ChocolateDispensedDelegate.cs
@@ -0,0 +1,4 @@
+namespace Mockolate.Migration.NSubstitutePlayground.Domain;
+
+/// Custom event delegate carried by .
+public delegate void ChocolateDispensedDelegate(string type, int amount);
diff --git a/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/ChocolateRecipe.cs b/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/ChocolateRecipe.cs
new file mode 100644
index 0000000..ea8e995
--- /dev/null
+++ b/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/ChocolateRecipe.cs
@@ -0,0 +1,19 @@
+namespace Mockolate.Migration.NSubstitutePlayground.Domain;
+
+/// Concrete recipe — used for partial mocks (NSubstitute ForPartsOf).
+public class ChocolateRecipe
+{
+ public virtual string Name { get; set; } = "Truffle";
+ public virtual int CocoaPercent { get; set; } = 70;
+
+ public virtual ChocolateBar Bake(int amount) =>
+ new(Name, CocoaPercent, amount * 1.5m);
+
+ public virtual bool Validate() => !string.IsNullOrEmpty(Name);
+
+ public virtual void Reset()
+ {
+ Name = "Truffle";
+ CocoaPercent = 70;
+ }
+}
diff --git a/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/ChocolateShop.cs b/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/ChocolateShop.cs
new file mode 100644
index 0000000..3e9a3d7
--- /dev/null
+++ b/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/ChocolateShop.cs
@@ -0,0 +1,52 @@
+namespace Mockolate.Migration.NSubstitutePlayground.Domain;
+
+/// The system-under-test that orchestrates the dependencies.
+public sealed class ChocolateShop
+{
+ private readonly IChocolateAuditor? _auditor;
+ private readonly IChocolateDispenser _dispenser;
+ private readonly IChocolateFactory _factory;
+
+ public ChocolateShop(
+ IChocolateDispenser dispenser,
+ IChocolateFactory factory,
+ IChocolateAuditor? auditor = null)
+ {
+ _dispenser = dispenser;
+ _factory = factory;
+ _auditor = auditor;
+ _dispenser.ChocolateDispensed += OnDispensed;
+ }
+
+ public int TotalSold { get; private set; }
+
+ public string DispenserName
+ {
+ get => _dispenser.Name;
+ set => _dispenser.Name = value;
+ }
+
+ public bool Sell(string type, int amount, decimal pricePerUnit = 1.5m)
+ {
+ if (!_dispenser.Dispense(type, amount))
+ {
+ return false;
+ }
+
+ _auditor?.RecordSale(type, amount, amount * pricePerUnit);
+ return true;
+ }
+
+ public Task SellAsync(string type, int amount)
+ => _dispenser.DispenseAsync(type, amount);
+
+ public Task RestockAsync(string recipe, int cocoa)
+ => _factory.BakeAsync(recipe, cocoa);
+
+ public int CheckStock(string type) => _dispenser[type];
+
+ public bool TryReserveStock(string type, out int reserved)
+ => _dispenser.TryReserve(type, out reserved);
+
+ private void OnDispensed(string type, int amount) => TotalSold += amount;
+}
diff --git a/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/IChocolateAuditor.cs b/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/IChocolateAuditor.cs
new file mode 100644
index 0000000..1c5b28f
--- /dev/null
+++ b/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/IChocolateAuditor.cs
@@ -0,0 +1,8 @@
+namespace Mockolate.Migration.NSubstitutePlayground.Domain;
+
+/// Used for multi-interface mocks (NSubstitute Substitute.For<T1,T2>()).
+public interface IChocolateAuditor
+{
+ int AuditCount { get; }
+ void RecordSale(string type, int amount, decimal total);
+}
diff --git a/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/IChocolateDispenser.cs b/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/IChocolateDispenser.cs
new file mode 100644
index 0000000..65c4e3f
--- /dev/null
+++ b/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/IChocolateDispenser.cs
@@ -0,0 +1,30 @@
+namespace Mockolate.Migration.NSubstitutePlayground.Domain;
+
+///
+/// Surface intended to exercise as much of Moq/NSubstitute as possible:
+/// indexer, properties (read/write), method overloads, async, ref, out, custom event, standard event.
+///
+public interface IChocolateDispenser
+{
+ int this[string type] { get; set; }
+ int TotalDispensed { get; set; }
+ string Name { get; set; }
+
+ bool Dispense(string type, int amount);
+ bool Dispense(string type);
+ Task DispenseAsync(string type, int amount);
+
+ /// Tries to reserve some stock for the given type, returning the reserved amount.
+ bool TryReserve(string type, out int reserved);
+
+ /// Refills stock; the caller passes a desired amount and gets the actual amount back via ref.
+ bool Refill(string type, ref int amount);
+
+ int CountByType(string type);
+
+ /// Void method — used to exercise NSubstitute When/Do.
+ void Notify(string type, int amount);
+
+ event ChocolateDispensedDelegate ChocolateDispensed;
+ event EventHandler StockLow;
+}
diff --git a/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/IChocolateFactory.cs b/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/IChocolateFactory.cs
new file mode 100644
index 0000000..d4daf83
--- /dev/null
+++ b/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/IChocolateFactory.cs
@@ -0,0 +1,10 @@
+namespace Mockolate.Migration.NSubstitutePlayground.Domain;
+
+/// Used to exercise async, generics on parameters, collection parameters.
+public interface IChocolateFactory
+{
+ int Capacity { get; }
+ Task BakeAsync(string recipe, int cocoa);
+ Task> BatchBakeAsync(IEnumerable recipes);
+ bool RegisterRecipe(string name);
+}
diff --git a/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/InvalidChocolateException.cs b/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/InvalidChocolateException.cs
new file mode 100644
index 0000000..88d1284
--- /dev/null
+++ b/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/InvalidChocolateException.cs
@@ -0,0 +1,8 @@
+namespace Mockolate.Migration.NSubstitutePlayground.Domain;
+
+/// Domain exception thrown for invalid chocolate operations.
+public class InvalidChocolateException : Exception
+{
+ public InvalidChocolateException() { }
+ public InvalidChocolateException(string message) : base(message) { }
+}
diff --git a/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/StaticAuditor.cs b/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/StaticAuditor.cs
new file mode 100644
index 0000000..118eccc
--- /dev/null
+++ b/Tests/Mockolate.Migration.NSubstitutePlayground/Domain/StaticAuditor.cs
@@ -0,0 +1,9 @@
+namespace Mockolate.Migration.NSubstitutePlayground.Domain;
+
+/// Concrete forwarding target — used for Substitute.ForTypeForwardingTo.
+public class StaticAuditor : IChocolateAuditor
+{
+ public int AuditCount { get; private set; }
+
+ public void RecordSale(string type, int amount, decimal total) => AuditCount++;
+}
diff --git a/Tests/Mockolate.Migration.NSubstitutePlayground/EventTests.cs b/Tests/Mockolate.Migration.NSubstitutePlayground/EventTests.cs
new file mode 100644
index 0000000..6caf335
--- /dev/null
+++ b/Tests/Mockolate.Migration.NSubstitutePlayground/EventTests.cs
@@ -0,0 +1,72 @@
+using Mockolate.Migration.NSubstitutePlayground.Domain;
+using NSubstitute;
+
+namespace Mockolate.Migration.NSubstitutePlayground;
+
+/// Event raising via the Raise helper.
+public class EventTests
+{
+ [Fact]
+ public async Task Raise_customDelegate_invokesSubscribedHandler()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ string? observedType = null;
+ int observedAmount = 0;
+ dispenser.ChocolateDispensed += (t, a) =>
+ {
+ observedType = t;
+ observedAmount = a;
+ };
+
+ dispenser.ChocolateDispensed += Raise.Event("Dark", 5);
+
+ await That(observedType).IsEqualTo("Dark");
+ await That(observedAmount).IsEqualTo(5);
+ }
+
+ [Fact]
+ public async Task Raise_eventHandlerWithArgsOnly_passesNullSender()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ int? observedLow = null;
+ dispenser.StockLow += (_, low) => observedLow = low;
+
+ dispenser.StockLow += Raise.Event>(null, 7);
+
+ await That(observedLow).IsEqualTo(7);
+ }
+
+ [Fact]
+ public async Task Raise_eventHandlerWithSenderAndArgs_passesBoth()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ object? observedSender = null;
+ int? observedLow = null;
+ dispenser.StockLow += (s, low) =>
+ {
+ observedSender = s;
+ observedLow = low;
+ };
+
+ dispenser.StockLow += Raise.Event>(dispenser, 2);
+
+ await That(observedSender).IsSameAs(dispenser);
+ await That(observedLow).IsEqualTo(2);
+ }
+
+ [Fact]
+ public async Task ShopSubscribesOnConstruction_andTracksDispensedAmounts()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ IChocolateFactory factory = Substitute.For();
+ dispenser.Dispense(Arg.Any(), Arg.Any()).Returns(true);
+ ChocolateShop shop = new(dispenser, factory);
+
+ shop.Sell("Dark", 2);
+ dispenser.ChocolateDispensed += Raise.Event("Dark", 2);
+ shop.Sell("Milk", 5);
+ dispenser.ChocolateDispensed += Raise.Event("Milk", 5);
+
+ await That(shop.TotalSold).IsEqualTo(7);
+ }
+}
diff --git a/Tests/Mockolate.Migration.NSubstitutePlayground/Mockolate.Migration.NSubstitutePlayground.csproj b/Tests/Mockolate.Migration.NSubstitutePlayground/Mockolate.Migration.NSubstitutePlayground.csproj
new file mode 100644
index 0000000..1ec0960
--- /dev/null
+++ b/Tests/Mockolate.Migration.NSubstitutePlayground/Mockolate.Migration.NSubstitutePlayground.csproj
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tests/Mockolate.Migration.NSubstitutePlayground/SetupTests.cs b/Tests/Mockolate.Migration.NSubstitutePlayground/SetupTests.cs
new file mode 100644
index 0000000..1e6b7d6
--- /dev/null
+++ b/Tests/Mockolate.Migration.NSubstitutePlayground/SetupTests.cs
@@ -0,0 +1,149 @@
+using Mockolate.Migration.NSubstitutePlayground.Domain;
+using NSubstitute;
+using NSubstitute.ExceptionExtensions;
+
+namespace Mockolate.Migration.NSubstitutePlayground;
+
+/// Setup patterns: Returns / Throws / sequence Returns / ReturnsForAnyArgs / ThrowsForAnyArgs / AndDoes.
+public class SetupTests
+{
+ [Fact]
+ public async Task NestedSubstituteSetup_chainedAcrossDependentSubstitutes()
+ {
+ // NSubstitute auto-creates child substitutes for properties returning interfaces / classes.
+ // Migration emits a TODO comment because Mockolate needs the child registered explicitly.
+ INested outer = Substitute.For();
+ outer.Inner.Inner.Name.Returns("deep");
+
+ await That(outer.Inner.Inner.Name).IsEqualTo("deep");
+ }
+
+ [Fact]
+ public async Task PropertyAssignment_returnsLastSetValue()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+
+ dispenser.Name = "ChocoMatic";
+
+ await That(dispenser.Name).IsEqualTo("ChocoMatic");
+ }
+
+ [Fact]
+ public async Task PropertyReturns_setsConfiguredValue()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ dispenser.Name.Returns("Choco-9000");
+
+ await That(dispenser.Name).IsEqualTo("Choco-9000");
+ }
+
+ [Fact]
+ public async Task Returns_directValue_dispensesAndShopRecordsTotal()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ IChocolateFactory factory = Substitute.For();
+ dispenser.Dispense("Dark", 3).Returns(true);
+ ChocolateShop shop = new(dispenser, factory);
+
+ bool sold = shop.Sell("Dark", 3);
+ dispenser.ChocolateDispensed += Raise.Event("Dark", 3);
+
+ await That(sold).IsTrue();
+ await That(shop.TotalSold).IsEqualTo(3);
+ }
+
+ [Fact]
+ public async Task Returns_factory_evaluatesPerCall()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ int counter = 1;
+ dispenser.CountByType(Arg.Any()).Returns(_ => counter);
+
+ int first = dispenser.CountByType("Dark");
+ counter = 5;
+ int second = dispenser.CountByType("Dark");
+
+ await That(first).IsEqualTo(1);
+ await That(second).IsEqualTo(5);
+ }
+
+ [Fact]
+ public async Task Returns_sequenceOfValues_returnsThemInOrder()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ dispenser.CountByType("Dark").Returns(1, 2, 3);
+
+ await That(dispenser.CountByType("Dark")).IsEqualTo(1);
+ await That(dispenser.CountByType("Dark")).IsEqualTo(2);
+ await That(dispenser.CountByType("Dark")).IsEqualTo(3);
+ }
+
+ [Fact]
+ public async Task ReturnsAndDoes_combinesReturnAndSideEffect()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ int sideEffect = 0;
+ dispenser.Dispense("Dark", 1).Returns(true).AndDoes(_ => sideEffect++);
+
+ _ = dispenser.Dispense("Dark", 1);
+ _ = dispenser.Dispense("Dark", 1);
+
+ await That(sideEffect).IsEqualTo(2);
+ }
+
+ [Fact]
+ public async Task ReturnsAsync_completesWithValue()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ dispenser.DispenseAsync("Dark", 1).Returns(Task.FromResult(true));
+
+ await That(await dispenser.DispenseAsync("Dark", 1)).IsTrue();
+ }
+
+ [Fact]
+ public async Task ReturnsForAnyArgs_ignoresArgumentsAtSetup()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ dispenser.Dispense("Dark", 1).ReturnsForAnyArgs(true);
+
+ await That(dispenser.Dispense("Milk", 99)).IsTrue();
+ await That(dispenser.Dispense("White", 1)).IsTrue();
+ }
+
+ [Fact]
+ public async Task Throws_genericException_isRaisedOnInvocation()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ dispenser.Dispense("reset", 0).Throws();
+
+ await That(() => dispenser.Dispense("reset", 0))
+ .Throws();
+ }
+
+ [Fact]
+ public async Task Throws_specificInstance_isRaisedOnInvocation()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ dispenser.Dispense("", 0).Throws(new InvalidChocolateException("empty type"));
+
+ await That(() => dispenser.Dispense("", 0))
+ .Throws()
+ .WithMessage("empty type");
+ }
+
+ [Fact]
+ public async Task ThrowsForAnyArgs_appliesEveryCall()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ dispenser.Dispense("ignored", 0).ThrowsForAnyArgs();
+
+ await That(() => dispenser.Dispense("Dark", 1))
+ .Throws();
+ }
+
+ public interface INested
+ {
+ INested Inner { get; }
+ string Name { get; }
+ }
+}
diff --git a/Tests/Mockolate.Migration.NSubstitutePlayground/UnsupportedFeatureTests.cs b/Tests/Mockolate.Migration.NSubstitutePlayground/UnsupportedFeatureTests.cs
new file mode 100644
index 0000000..64fde60
--- /dev/null
+++ b/Tests/Mockolate.Migration.NSubstitutePlayground/UnsupportedFeatureTests.cs
@@ -0,0 +1,156 @@
+using Mockolate.Migration.NSubstitutePlayground.Domain;
+using NSubstitute;
+using NSubstitute.ClearExtensions;
+using NSubstitute.ExceptionExtensions;
+using NSubstitute.Extensions;
+
+namespace Mockolate.Migration.NSubstitutePlayground;
+
+///
+/// NSubstitute features the migration does NOT yet handle. They still pass against NSubstitute;
+/// run the migration to see what is/isn't transformed and where manual rewrites are needed.
+///
+public class UnsupportedFeatureTests
+{
+ // NOT YET MIGRATED: Arg.Do(action) — capture each invocation's argument
+ [Fact]
+ public async Task ArgDo_capturesEveryInvocationArgument()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ List amounts = new();
+ dispenser.Dispense("Dark", Arg.Do(a => amounts.Add(a))).Returns(true);
+
+ _ = dispenser.Dispense("Dark", 2);
+ _ = dispenser.Dispense("Dark", 5);
+
+ await That(amounts).IsEqualTo([2, 5,]).InAnyOrder();
+ }
+
+ // NOT YET MIGRATED: Arg.Invoke<...> to call back into a delegate parameter
+ [Fact]
+ public async Task ArgInvoke_callsThroughDelegateParameter()
+ {
+ // Use a mini-substitute with an Action parameter to exercise Arg.Invoke.
+ IInvokeTarget target = Substitute.For();
+ int called = 0;
+ target.Run(Arg.Invoke()); // when target.Run(action) is called, NSubstitute invokes action()
+
+ target.Run(() => called++);
+
+ await That(called).IsEqualTo(1);
+ }
+
+ // NOT YET MIGRATED: CallInfo argument access via x => x.Arg() / x.ArgAt(index)
+ [Fact]
+ public async Task CallInfo_argumentAccessInReturns()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ dispenser.Dispense(Arg.Any(), Arg.Any())
+ .Returns(call =>
+ {
+ string type = call.Arg();
+ int amount = call.ArgAt(1);
+ return type == "Dark" && amount > 0;
+ });
+
+ await That(dispenser.Dispense("Dark", 4)).IsTrue();
+ await That(dispenser.Dispense("Milk", 4)).IsFalse();
+ await That(dispenser.Dispense("Dark", 0)).IsFalse();
+ }
+
+ // NOT YET MIGRATED: ClearSubstitute — removes setups and call history together
+ [Fact]
+ public async Task ClearSubstitute_resetsBothCallsAndSetups()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ dispenser.Dispense("Dark", 1).Returns(true);
+ _ = dispenser.Dispense("Dark", 1);
+
+ dispenser.ClearSubstitute();
+
+ // After clear: no calls remembered, and the previous Returns(true) is gone.
+ dispenser.DidNotReceive().Dispense("Dark", 1);
+ await That(dispenser.Dispense("Dark", 1)).IsFalse();
+ }
+
+ // NOT YET MIGRATED: Configure() — re-enter setup mode after calls have been recorded
+ [Fact]
+ public async Task Configure_changesReturnAfterUse()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ dispenser.Dispense("Dark", 1).Returns(false);
+
+ _ = dispenser.Dispense("Dark", 1);
+ dispenser.Configure().Dispense("Dark", 1).Returns(true);
+
+ await That(dispenser.Dispense("Dark", 1)).IsTrue();
+ }
+
+ // NOT YET MIGRATED: Out parameters via Arg.Any() with discard pattern
+ [Fact]
+ public async Task OutParameter_isSetByCallback()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ dispenser
+ .TryReserve(Arg.Any(), out Arg.Any())
+ .Returns(call =>
+ {
+ call[1] = 7;
+ return true;
+ });
+
+ bool ok = dispenser.TryReserve("Dark", out int reserved);
+
+ await That(ok).IsTrue();
+ await That(reserved).IsEqualTo(7);
+ }
+
+ // NOT YET MIGRATED: Received.InOrder for ordered cross-substitute verification
+ [Fact]
+ public async Task ReceivedInOrder_ordersAcrossSubstitutes()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ IChocolateAuditor auditor = Substitute.For();
+ dispenser.Dispense(Arg.Any(), Arg.Any()).Returns(true);
+
+ _ = dispenser.Dispense("Dark", 1);
+ auditor.RecordSale("Dark", 1, 1.5m);
+
+ Received.InOrder(() =>
+ {
+ dispenser.Dispense("Dark", 1);
+ auditor.RecordSale("Dark", 1, 1.5m);
+ });
+ }
+
+ // NOT YET MIGRATED: ReturnsNull / ReturnsNullForAnyArgs (extension methods)
+ [Fact]
+ public async Task ReturnsNull_isShortcutForReturnsDefault()
+ {
+ IChocolateFactory factory = Substitute.For();
+ factory.BatchBakeAsync(Arg.Any>()).Returns(Task.FromResult>(null!));
+
+ IReadOnlyList result = await factory.BatchBakeAsync(["Dark", "Milk",]);
+ await That(result).IsNull();
+ }
+
+ // NOT YET MIGRATED: ThrowsAsync extension on async setups
+ [Fact]
+ public async Task ThrowsAsync_appliesToAwaitedTask()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ dispenser.DispenseAsync("Dark", 1).ThrowsAsync(new TimeoutException());
+
+ Task Act()
+ {
+ return dispenser.DispenseAsync("Dark", 1);
+ }
+
+ await That((Func>)Act).Throws();
+ }
+
+ public interface IInvokeTarget
+ {
+ void Run(Action action);
+ }
+}
diff --git a/Tests/Mockolate.Migration.NSubstitutePlayground/Usings.cs b/Tests/Mockolate.Migration.NSubstitutePlayground/Usings.cs
new file mode 100644
index 0000000..88f147d
--- /dev/null
+++ b/Tests/Mockolate.Migration.NSubstitutePlayground/Usings.cs
@@ -0,0 +1,7 @@
+global using System;
+global using System.Collections.Generic;
+global using System.Threading;
+global using System.Threading.Tasks;
+global using Xunit;
+global using aweXpect;
+global using static aweXpect.Expect;
diff --git a/Tests/Mockolate.Migration.NSubstitutePlayground/VerifyTests.cs b/Tests/Mockolate.Migration.NSubstitutePlayground/VerifyTests.cs
new file mode 100644
index 0000000..857caf0
--- /dev/null
+++ b/Tests/Mockolate.Migration.NSubstitutePlayground/VerifyTests.cs
@@ -0,0 +1,103 @@
+using Mockolate.Migration.NSubstitutePlayground.Domain;
+using NSubstitute;
+
+namespace Mockolate.Migration.NSubstitutePlayground;
+
+/// Verification patterns: Received / DidNotReceive / Received(n) / WithAnyArgs.
+public class VerifyTests
+{
+ [Fact]
+ public void ClearReceivedCalls_resetsHistory()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ dispenser.Dispense(Arg.Any(), Arg.Any()).Returns(true);
+
+ dispenser.Dispense("Dark", 1);
+ dispenser.ClearReceivedCalls();
+ dispenser.Dispense("Dark", 2);
+
+ dispenser.Received(1).Dispense(Arg.Any(), Arg.Any());
+ }
+
+ [Fact]
+ public void DidNotReceive_method_wasNeverCalled()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ IChocolateFactory factory = Substitute.For();
+ dispenser.Dispense("Dark", 1).Returns(false);
+ ChocolateShop shop = new(dispenser, factory);
+
+ shop.Sell("Dark", 1);
+
+ dispenser.DidNotReceive().Dispense("Milk", Arg.Any());
+ }
+
+ [Fact]
+ public void DidNotReceiveWithAnyArgs_matchesNoInvocation()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+
+ dispenser.DidNotReceiveWithAnyArgs().Dispense(default!, default);
+ }
+
+ [Fact]
+ public void Received_method_wasCalledOnce()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ IChocolateFactory factory = Substitute.For();
+ dispenser.Dispense("Dark", 2).Returns(true);
+ ChocolateShop shop = new(dispenser, factory);
+
+ shop.Sell("Dark", 2);
+
+ dispenser.Received().Dispense("Dark", 2);
+ }
+
+ [Fact]
+ public void ReceivedExactCount_isHonored()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ dispenser.Dispense(Arg.Any(), Arg.Any()).Returns(true);
+
+ dispenser.Dispense("Dark", 1);
+ dispenser.Dispense("Dark", 2);
+ dispenser.Dispense("Dark", 3);
+
+ dispenser.Received(3).Dispense("Dark", Arg.Any());
+ }
+
+ [Fact]
+ public void ReceivedProperty_was_read()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ dispenser.Name.Returns("Choc-Box");
+
+ _ = dispenser.Name;
+ _ = dispenser.Name;
+
+ _ = dispenser.Received(2).Name;
+ }
+
+ [Fact]
+ public void ReceivedProperty_was_set()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+
+ dispenser.Name = "Choco-2025";
+
+ dispenser.Received().Name = "Choco-2025";
+ }
+
+ [Fact]
+ public void ReceivedWithAnyArgs_matchesAllInvocations()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ dispenser.Dispense(Arg.Any(), Arg.Any()).Returns(true);
+
+ dispenser.Dispense("Dark", 1);
+ dispenser.Dispense("Milk", 5);
+
+ // Arguments below are placeholders; ReceivedWithAnyArgs ignores them.
+ dispenser.ReceivedWithAnyArgs(2).Dispense(default!, default);
+ }
+}
diff --git a/Tests/Mockolate.Migration.NSubstitutePlayground/WhenDoTests.cs b/Tests/Mockolate.Migration.NSubstitutePlayground/WhenDoTests.cs
new file mode 100644
index 0000000..e854892
--- /dev/null
+++ b/Tests/Mockolate.Migration.NSubstitutePlayground/WhenDoTests.cs
@@ -0,0 +1,49 @@
+using Mockolate.Migration.NSubstitutePlayground.Domain;
+using NSubstitute;
+
+namespace Mockolate.Migration.NSubstitutePlayground;
+
+/// When/Do — used to attach side effects to void members and to substitute for partial mocks.
+public class WhenDoTests
+{
+ [Fact]
+ public async Task When_DoNotCallBase_disablesPartialBaseInvocation()
+ {
+ // ForPartsOf normally calls real virtual methods. DoNotCallBase suppresses that.
+ ChocolateRecipe recipe = Substitute.ForPartsOf();
+ recipe.When(r => r.Reset())
+ .DoNotCallBase();
+
+ recipe.Name = "Praline";
+ recipe.Reset(); // would normally reset Name to "Truffle"
+
+ await That(recipe.Name).IsEqualTo("Praline");
+ }
+
+ [Fact]
+ public async Task When_voidMethod_runsCallback()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ string? captured = null;
+ dispenser.When(d => d.Notify("Dark", 1))
+ .Do(_ => captured = "ran");
+
+ dispenser.Notify("Dark", 1);
+
+ await That(captured).IsEqualTo("ran");
+ }
+
+ [Fact]
+ public async Task WhenForAnyArgs_voidMethod_runsCallbackForAnyInvocation()
+ {
+ IChocolateDispenser dispenser = Substitute.For();
+ int count = 0;
+ dispenser.WhenForAnyArgs(d => d.Notify(default!, default))
+ .Do(_ => count++);
+
+ dispenser.Notify("Dark", 1);
+ dispenser.Notify("Milk", 9);
+
+ await That(count).IsEqualTo(2);
+ }
+}