Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Mockolate.Migration.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
<Folder Name="/Pipeline/">
<Project Path="Pipeline/Build.csproj" />
</Folder>
<Folder Name="/Playground/">
<Project Path="Tests/Mockolate.Migration.MoqPlayground/Mockolate.Migration.MoqPlayground.csproj" />
<Project Path="Tests/Mockolate.Migration.NSubstitutePlayground/Mockolate.Migration.NSubstitutePlayground.csproj" />
</Folder>
<Folder Name="/Tests/">
<Project Path="Tests/Mockolate.Migration.Example.Tests/Mockolate.Migration.Example.Tests.csproj" />
<Project Path="Tests/Mockolate.Migration.Tests/Mockolate.Migration.Tests.csproj" />
Expand Down
2 changes: 2 additions & 0 deletions Pipeline/Build.UnitTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ partial class Build

Project[] UnitTestProjects =>
[
Solution.Playground.Mockolate_Migration_MoqPlayground,
Solution.Playground.Mockolate_Migration_NSubstitutePlayground,
Solution.Tests.Mockolate_Migration_Tests,
];

Expand Down
98 changes: 98 additions & 0 deletions Tests/Mockolate.Migration.MoqPlayground/ArgumentMatcherTests.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>Argument matchers: It.IsAny / It.Is / It.IsRegex / It.IsInRange / It.IsNotNull / It.Ref / out.</summary>
public class ArgumentMatcherTests
{
[Fact]
public async Task ItIs_predicate_matchesEvenAmounts()
{
Mock<IChocolateDispenser> dispenser = new();
dispenser.Setup(d => d.Dispense("Dark", It.Is<int>(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<IChocolateDispenser> dispenser = new();
dispenser.Setup(d => d.Dispense(It.IsAny<string>(), It.IsAny<int>())).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<IChocolateDispenser> dispenser = new();

Check warning on line 36 in Tests/Mockolate.Migration.MoqPlayground/ArgumentMatcherTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Moq should be migrated to Mockolate.

See more on https://sonarcloud.io/project/issues?id=aweXpect_Mockolate.Migration&issues=AZ3kWvz-NLg40HRgc89_&open=AZ3kWvz-NLg40HRgc89_&pullRequest=57
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<IChocolateFactory> factory = new();
factory.Setup(f => f.RegisterRecipe(It.IsNotNull<string>())).Returns(true);

await That(factory.Object.RegisterRecipe("Pralines")).IsTrue();
await That(factory.Object.RegisterRecipe(null!)).IsFalse();
}

[Fact]
public async Task ItIsRegex_matchesPattern()
{
Mock<IChocolateFactory> factory = new();

Check warning on line 57 in Tests/Mockolate.Migration.MoqPlayground/ArgumentMatcherTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Moq should be migrated to Mockolate.

See more on https://sonarcloud.io/project/issues?id=aweXpect_Mockolate.Migration&issues=AZ3kWvz-NLg40HRgc8-B&open=AZ3kWvz-NLg40HRgc8-B&pullRequest=57
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<IChocolateDispenser> 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<IChocolateDispenser> 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<IChocolateDispenser> dispenser = new();

Check warning on line 90 in Tests/Mockolate.Migration.MoqPlayground/ArgumentMatcherTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Moq should be migrated to Mockolate.

See more on https://sonarcloud.io/project/issues?id=aweXpect_Mockolate.Migration&issues=AZ3kWvz-NLg40HRgc8-E&open=AZ3kWvz-NLg40HRgc8-E&pullRequest=57
dispenser.Setup(d => d.Refill("Dark", ref It.Ref<int>.IsAny)).Returns(true);

int amount = 10;
bool ok = dispenser.Object.Refill("Dark", ref amount);

await That(ok).IsTrue();
}
}
59 changes: 59 additions & 0 deletions Tests/Mockolate.Migration.MoqPlayground/CreationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using Mockolate.Migration.MoqPlayground.Domain;
using Moq;

namespace Mockolate.Migration.MoqPlayground;

using MockBehavior = Moq.MockBehavior;

/// <summary>Mock construction patterns.</summary>
public class CreationTests
{
[Fact]
public async Task ClassMockWithConstructorArgs_isCreatedWithThoseArgs()
{
// ChocolateRecipe has a parameterless ctor, but Moq supports passing args to base.
Mock<ChocolateRecipe> recipe = new();

Check warning on line 15 in Tests/Mockolate.Migration.MoqPlayground/CreationTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Moq should be migrated to Mockolate.

See more on https://sonarcloud.io/project/issues?id=aweXpect_Mockolate.Migration&issues=AZ3kWvzjNLg40HRgc89o&open=AZ3kWvzjNLg40HRgc89o&pullRequest=57
recipe.SetupGet(r => r.Name).Returns("Praline");

await That(recipe.Object.Name).IsEqualTo("Praline");
}

[Fact]
public async Task DefaultLooseMock_returnsDefaultsForUnsetMembers()
{
Mock<IChocolateDispenser> 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<IChocolateDispenser> dispenser = new(MockBehavior.Loose);

Check warning on line 35 in Tests/Mockolate.Migration.MoqPlayground/CreationTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Moq should be migrated to Mockolate.

See more on https://sonarcloud.io/project/issues?id=aweXpect_Mockolate.Migration&issues=AZ3kWvzjNLg40HRgc89q&open=AZ3kWvzjNLg40HRgc89q&pullRequest=57

await That(dispenser.Object.Dispense("Dark", 1)).IsFalse();
}

[Fact]
public async Task ObjectAccess_isUsedToReachTheMockedInstance()
{
Mock<IChocolateFactory> factory = new();

Check warning on line 43 in Tests/Mockolate.Migration.MoqPlayground/CreationTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Moq should be migrated to Mockolate.

See more on https://sonarcloud.io/project/issues?id=aweXpect_Mockolate.Migration&issues=AZ3kWvzjNLg40HRgc89r&open=AZ3kWvzjNLg40HRgc89r&pullRequest=57
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<IChocolateDispenser> dispenser = new(MockBehavior.Strict);

await That(() => dispenser.Object.Dispense("Dark", 1))
.Throws<MockException>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace Mockolate.Migration.MoqPlayground.Domain;

/// <summary>A baked chocolate bar.</summary>
public sealed record ChocolateBar(string Type, int Cocoa, decimal Price);
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace Mockolate.Migration.MoqPlayground.Domain;

/// <summary>Custom event delegate carried by <see cref="IChocolateDispenser" />.</summary>
public delegate void ChocolateDispensedDelegate(string type, int amount);
18 changes: 18 additions & 0 deletions Tests/Mockolate.Migration.MoqPlayground/Domain/ChocolateRecipe.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Mockolate.Migration.MoqPlayground.Domain;

/// <summary>Concrete recipe — used for partial mocks (Moq <c>CallBase</c>, NSubstitute <c>ForPartsOf</c>).</summary>
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);

/// <summary>Used for Moq <c>Protected()</c>.</summary>
protected virtual int InternalSecret() => 42;

public int CallInternalSecret() => InternalSecret();
}
52 changes: 52 additions & 0 deletions Tests/Mockolate.Migration.MoqPlayground/Domain/ChocolateShop.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
namespace Mockolate.Migration.MoqPlayground.Domain;

/// <summary>The system-under-test that orchestrates the dependencies.</summary>
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<bool> SellAsync(string type, int amount)
=> _dispenser.DispenseAsync(type, amount);

public Task<ChocolateBar> 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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Mockolate.Migration.MoqPlayground.Domain;

/// <summary>Used for multi-interface mocks (Moq <c>As&lt;T&gt;()</c>, NSubstitute <c>Substitute.For&lt;T1,T2&gt;()</c>).</summary>
public interface IChocolateAuditor
{
int AuditCount { get; }
void RecordSale(string type, int amount, decimal total);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace Mockolate.Migration.MoqPlayground.Domain;

/// <summary>
/// Surface intended to exercise as much of Moq/NSubstitute as possible:
/// indexer, properties (read/write), method overloads, async, ref, out, custom event, standard event.
/// </summary>
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<bool> DispenseAsync(string type, int amount);

/// <summary>Tries to reserve some stock for the given type, returning the reserved amount.</summary>
bool TryReserve(string type, out int reserved);

/// <summary>Refills stock; the caller passes a desired amount and gets the actual amount back via ref.</summary>
bool Refill(string type, ref int amount);

int CountByType(string type);

event ChocolateDispensedDelegate ChocolateDispensed;
event EventHandler<int> StockLow;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Mockolate.Migration.MoqPlayground.Domain;

/// <summary>Used to exercise async, generics on parameters, collection parameters.</summary>
public interface IChocolateFactory
{
int Capacity { get; }
Task<ChocolateBar> BakeAsync(string recipe, int cocoa);
Task<IReadOnlyList<ChocolateBar>> BatchBakeAsync(IEnumerable<string> recipes);
bool RegisterRecipe(string name);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Mockolate.Migration.MoqPlayground.Domain;

/// <summary>Domain exception thrown for invalid chocolate operations.</summary>
public class InvalidChocolateException : Exception
{
public InvalidChocolateException() { }
public InvalidChocolateException(string message) : base(message) { }
}
80 changes: 80 additions & 0 deletions Tests/Mockolate.Migration.MoqPlayground/EventTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using Mockolate.Migration.MoqPlayground.Domain;
using Moq;

namespace Mockolate.Migration.MoqPlayground;

using It = Moq.It;

/// <summary>Event subscription / raising / verification.</summary>
public class EventTests
{
[Fact]
public async Task Raise_customDelegate_invokesSubscribedHandler()
{
Mock<IChocolateDispenser> dispenser = new();

Check warning on line 14 in Tests/Mockolate.Migration.MoqPlayground/EventTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Moq should be migrated to Mockolate.

See more on https://sonarcloud.io/project/issues?id=aweXpect_Mockolate.Migration&issues=AZ3kWvzsNLg40HRgc89t&open=AZ3kWvzsNLg40HRgc89t&pullRequest=57
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<IChocolateDispenser> 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<IChocolateDispenser> dispenser = new();
Mock<IChocolateFactory> factory = new();
dispenser.Setup(d => d.Dispense(It.IsAny<string>(), It.IsAny<int>())).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<ChocolateDispensedDelegate>(),
Times.Once());
}

[Fact]
public async Task VerifyAdd_recordsSubscription()
{
Mock<IChocolateDispenser> dispenser = new();

Check warning on line 63 in Tests/Mockolate.Migration.MoqPlayground/EventTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Moq should be migrated to Mockolate.

See more on https://sonarcloud.io/project/issues?id=aweXpect_Mockolate.Migration&issues=AZ3kWvzsNLg40HRgc89x&open=AZ3kWvzsNLg40HRgc89x&pullRequest=57
ChocolateDispensedDelegate handler = (_, _) => { };
dispenser.Object.ChocolateDispensed += handler;

dispenser.VerifyAdd(d => d.ChocolateDispensed += It.IsAny<ChocolateDispensedDelegate>(), Times.Once());
}

[Fact]
public async Task VerifyRemove_recordsUnsubscription()
{
Mock<IChocolateDispenser> dispenser = new();
ChocolateDispensedDelegate handler = (_, _) => { };
dispenser.Object.ChocolateDispensed += handler;
dispenser.Object.ChocolateDispensed -= handler;

dispenser.VerifyRemove(d => d.ChocolateDispensed -= It.IsAny<ChocolateDispensedDelegate>(), Times.Once());
}
}
Loading
Loading