From 293665883fc7b69dea84244fa5ef6c74fd9cbb8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sat, 31 Jan 2026 13:30:46 +0100 Subject: [PATCH 1/2] feat: add simpler constructor for `MockMonitor` --- .../03-monitor-interactions.md | 19 +++++++++++++++-- README.md | 19 +++++++++++++++-- Source/Mockolate/Monitor/MockMonitor.cs | 11 ++++++++++ .../Expected/Mockolate_net10.0.txt | 1 + .../Expected/Mockolate_net8.0.txt | 1 + .../Expected/Mockolate_netstandard2.0.txt | 1 + .../Monitor/MockMonitorTests.cs | 21 +++++++------------ 7 files changed, 55 insertions(+), 18 deletions(-) diff --git a/Docs/pages/advanced-features/03-monitor-interactions.md b/Docs/pages/advanced-features/03-monitor-interactions.md index 8386e9c0..5d531005 100644 --- a/Docs/pages/advanced-features/03-monitor-interactions.md +++ b/Docs/pages/advanced-features/03-monitor-interactions.md @@ -5,10 +5,10 @@ can use a `MockMonitor`: ```csharp var sut = Mock.Create(); +var monitor = new MockMonitor(sut); sut.Dispense("Dark", 1); // Not monitored -var monitorScope = sut.MonitorMock(out var monitor); -using (monitorScope) +using (monitor.Run()) { sut.Dispense("Dark", 2); // Monitored } @@ -18,6 +18,21 @@ sut.Dispense("Dark", 3); // Not monitored monitor.Verify.Invoked.Dispense(It.Is("Dark"), It.IsAny()).Once(); ``` +Alternatively, you can use the `MonitorMock()` extension method to create an already running monitor directly from the +mock: + +```csharp +var sut = Mock.Create(); + +sut.Dispense("Dark", 1); // Not monitored +using var scope = sut.MonitorMock(out var monitor); +sut.Dispense("Dark", 2); // Monitored +sut.Dispense("Dark", 3); // Not monitored + +// Verifications on the monitor only count interactions during the lifetime scope of the `IDisposable` +monitor.Verify.Invoked.Dispense(It.Is("Dark"), It.IsAny()).Twice(); +``` + ## Clear all interactions For simpler scenarios you can directly clear all recorded interactions on a mock using `ClearAllInteractions` on the diff --git a/README.md b/README.md index 3a0ddf3c..19ed1550 100644 --- a/README.md +++ b/README.md @@ -743,10 +743,10 @@ can use a `MockMonitor`: ```csharp var sut = Mock.Create(); +var monitor = new MockMonitor(sut); sut.Dispense("Dark", 1); // Not monitored -var monitorScope = sut.MonitorMock(out var monitor); -using (monitorScope) +using (monitor.Run()) { sut.Dispense("Dark", 2); // Monitored } @@ -756,6 +756,21 @@ sut.Dispense("Dark", 3); // Not monitored monitor.Verify.Invoked.Dispense(It.Is("Dark"), It.IsAny()).Once(); ``` +Alternatively, you can use the `MonitorMock()` extension method to create an already running monitor directly from the +mock: + +```csharp +var sut = Mock.Create(); + +sut.Dispense("Dark", 1); // Not monitored +using var scope = sut.MonitorMock(out var monitor); +sut.Dispense("Dark", 2); // Monitored +sut.Dispense("Dark", 3); // Not monitored + +// Verifications on the monitor only count interactions during the lifetime scope of the `IDisposable` +monitor.Verify.Invoked.Dispense(It.Is("Dark"), It.IsAny()).Twice(); +``` + #### Clear all interactions For simpler scenarios you can directly clear all recorded interactions on a mock using `ClearAllInteractions` on the diff --git a/Source/Mockolate/Monitor/MockMonitor.cs b/Source/Mockolate/Monitor/MockMonitor.cs index a3a3dea6..41958f72 100644 --- a/Source/Mockolate/Monitor/MockMonitor.cs +++ b/Source/Mockolate/Monitor/MockMonitor.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Mockolate.Exceptions; using Mockolate.Interactions; using Mockolate.Verify; @@ -112,6 +113,16 @@ public MockMonitor(Mock mock) : base(mock.Interactions) new MockRegistration(mock.Registrations.Behavior, mock.Registrations.Prefix, Interactions)); } + /// + public MockMonitor(T mock) : this(mock as IMockSubject) + { + } + + private MockMonitor(IMockSubject? mockSubject) + : this(mockSubject?.Mock ?? throw new MockException("The subject is no mock.")) + { + } + /// /// Verifies the interactions with the mocked subject of . /// diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt index 7cfa92cd..b3509efc 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt @@ -569,6 +569,7 @@ namespace Mockolate.Monitor public sealed class MockMonitor : Mockolate.Monitor.MockMonitor { public MockMonitor(Mockolate.Mock mock) { } + public MockMonitor(T mock) { } public Mockolate.Verify.IMockVerify Verify { get; } } } diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt index 893e0cab..cd26a7bf 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt @@ -568,6 +568,7 @@ namespace Mockolate.Monitor public sealed class MockMonitor : Mockolate.Monitor.MockMonitor { public MockMonitor(Mockolate.Mock mock) { } + public MockMonitor(T mock) { } public Mockolate.Verify.IMockVerify Verify { get; } } } diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt index 4db24ded..f1a1c10b 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt @@ -535,6 +535,7 @@ namespace Mockolate.Monitor public sealed class MockMonitor : Mockolate.Monitor.MockMonitor { public MockMonitor(Mockolate.Mock mock) { } + public MockMonitor(T mock) { } public Mockolate.Verify.IMockVerify Verify { get; } } } diff --git a/Tests/Mockolate.Tests/Monitor/MockMonitorTests.cs b/Tests/Mockolate.Tests/Monitor/MockMonitorTests.cs index 9dcd78c0..343137df 100644 --- a/Tests/Mockolate.Tests/Monitor/MockMonitorTests.cs +++ b/Tests/Mockolate.Tests/Monitor/MockMonitorTests.cs @@ -9,8 +9,7 @@ public sealed class MockMonitorTests public async Task ClearAllInteractions_WhenMonitorIsRunning_ShouldClearInternalCollection() { IMyService sut = Mock.Create(); - Mock mock = ((IMockSubject)sut).Mock; - MockMonitor monitor = new(mock); + MockMonitor monitor = new(sut); sut.IsValid(1); using IDisposable disposable = monitor.Run(); @@ -24,8 +23,7 @@ public async Task ClearAllInteractions_WhenMonitorIsRunning_ShouldClearInternalC public async Task DisposeTwice_ShouldNotIncludeMoreInvocations() { IMyService sut = Mock.Create(); - Mock mock = ((IMockSubject)sut).Mock; - MockMonitor monitor = new(mock); + MockMonitor monitor = new(sut); sut.IsValid(1); sut.IsValid(2); @@ -53,8 +51,7 @@ public async Task DisposeTwice_ShouldNotIncludeMoreInvocations() public async Task MultipleRun_ShouldMonitorInvocationsDuringTheRun() { IMyService sut = Mock.Create(); - Mock mock = ((IMockSubject)sut).Mock; - MockMonitor monitor = new(mock); + MockMonitor monitor = new(sut); sut.IsValid(1); sut.IsValid(2); @@ -91,8 +88,7 @@ public async Task MultipleRun_ShouldMonitorInvocationsDuringTheRun() public async Task NestedRun_ShouldThrowInvalidOperationException() { IMyService sut = Mock.Create(); - Mock mock = ((IMockSubject)sut).Mock; - MockMonitor monitor = new(mock); + MockMonitor monitor = new(sut); void Act() { @@ -113,8 +109,7 @@ await That(Act).Throws() public async Task Run_ShouldIncludeAllInvocations() { IMyService sut = Mock.Create(); - Mock mock = ((IMockSubject)sut).Mock; - MockMonitor monitor = new(mock); + MockMonitor monitor = new(sut); using (monitor.Run()) { @@ -134,8 +129,7 @@ public async Task Run_ShouldIncludeAllInvocations() public async Task Run_ShouldMonitorInvocationsDuringTheRun() { IMyService sut = Mock.Create(); - Mock mock = ((IMockSubject)sut).Mock; - MockMonitor monitor = new(mock); + MockMonitor monitor = new(sut); sut.IsValid(1); sut.IsValid(2); @@ -163,8 +157,7 @@ public async Task Run_ShouldMonitorInvocationsDuringTheRun() public async Task Verify_WhileRunning_ShouldBeUpToDate() { IMyService sut = Mock.Create(); - Mock mock = ((IMockSubject)sut).Mock; - MockMonitor monitor = new(mock); + MockMonitor monitor = new(sut); using (monitor.Run()) { From c13464dffbb31d86f4acc385723a510b7135c7bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sat, 31 Jan 2026 13:43:19 +0100 Subject: [PATCH 2/2] Fix review issues --- .../advanced-features/03-monitor-interactions.md | 2 +- README.md | 2 +- .../Sources/Sources.ForMock.Extensions.cs | 8 ++++---- Source/Mockolate/Monitor/MockMonitor.cs | 16 ++++++---------- .../Expected/Mockolate_net10.0.txt | 1 - .../Expected/Mockolate_net8.0.txt | 1 - .../Expected/Mockolate_netstandard2.0.txt | 1 - .../Mockolate.Tests/Monitor/MockMonitorTests.cs | 15 +++++++++++++++ 8 files changed, 27 insertions(+), 19 deletions(-) diff --git a/Docs/pages/advanced-features/03-monitor-interactions.md b/Docs/pages/advanced-features/03-monitor-interactions.md index 5d531005..2dd0b116 100644 --- a/Docs/pages/advanced-features/03-monitor-interactions.md +++ b/Docs/pages/advanced-features/03-monitor-interactions.md @@ -27,7 +27,7 @@ var sut = Mock.Create(); sut.Dispense("Dark", 1); // Not monitored using var scope = sut.MonitorMock(out var monitor); sut.Dispense("Dark", 2); // Monitored -sut.Dispense("Dark", 3); // Not monitored +sut.Dispense("Dark", 3); // Monitored // Verifications on the monitor only count interactions during the lifetime scope of the `IDisposable` monitor.Verify.Invoked.Dispense(It.Is("Dark"), It.IsAny()).Twice(); diff --git a/README.md b/README.md index 19ed1550..7053a80b 100644 --- a/README.md +++ b/README.md @@ -765,7 +765,7 @@ var sut = Mock.Create(); sut.Dispense("Dark", 1); // Not monitored using var scope = sut.MonitorMock(out var monitor); sut.Dispense("Dark", 2); // Monitored -sut.Dispense("Dark", 3); // Not monitored +sut.Dispense("Dark", 3); // Monitored // Verifications on the monitor only count interactions during the lifetime scope of the `IDisposable` monitor.Verify.Invoked.Dispense(It.Is("Dark"), It.IsAny()).Twice(); diff --git a/Source/Mockolate.SourceGenerators/Sources/Sources.ForMock.Extensions.cs b/Source/Mockolate.SourceGenerators/Sources/Sources.ForMock.Extensions.cs index 5df764d2..aa7fcb40 100644 --- a/Source/Mockolate.SourceGenerators/Sources/Sources.ForMock.Extensions.cs +++ b/Source/Mockolate.SourceGenerators/Sources/Sources.ForMock.Extensions.cs @@ -51,10 +51,10 @@ namespace Mockolate; .Append(@class.ClassFullName.EscapeForXmlDoc()).Append("\" />.").AppendLine(); sb.Append("\t\t/// ").AppendLine(); sb.Append("\t\tpublic System.IDisposable MonitorMock(out MockMonitor<").Append(@class.ClassFullName) - .AppendLine("> monitor)").AppendLine(); + .Append("> monitor)").AppendLine(); sb.Append("\t\t{").AppendLine(); sb.Append("\t\t\tmonitor = new MockMonitor<").Append(@class.ClassFullName) - .AppendLine(">(GetMockOrThrow(subject));").AppendLine(); + .AppendLine(">(subject);").AppendLine(); sb.Append("\t\t\treturn monitor.Run();").AppendLine(); sb.Append("\t\t}").AppendLine(); sb.AppendLine("\t}"); @@ -203,10 +203,10 @@ private static Mock GetMockOrThrow(T subject) where T : System.Delegate .Append(@class.ClassFullName.EscapeForXmlDoc()).Append("\" />.").AppendLine(); sb.Append("\t\t/// ").AppendLine(); sb.Append("\t\tpublic System.IDisposable MonitorMock(out MockMonitor<").Append(@class.ClassFullName) - .AppendLine("> monitor)").AppendLine(); + .Append("> monitor)").AppendLine(); sb.Append("\t\t{").AppendLine(); sb.Append("\t\t\tmonitor = new MockMonitor<").Append(@class.ClassFullName) - .AppendLine(">(GetMockOrThrow(subject));").AppendLine(); + .AppendLine(">(subject);").AppendLine(); sb.Append("\t\t\treturn monitor.Run();").AppendLine(); sb.Append("\t\t}").AppendLine(); sb.AppendLine("\t}"); diff --git a/Source/Mockolate/Monitor/MockMonitor.cs b/Source/Mockolate/Monitor/MockMonitor.cs index 41958f72..e9d7d408 100644 --- a/Source/Mockolate/Monitor/MockMonitor.cs +++ b/Source/Mockolate/Monitor/MockMonitor.cs @@ -101,26 +101,22 @@ private sealed class MonitorScope(Action callback) : IDisposable /// /// /// Use this class to track and analyze interactions with a mock, such as which members were accessed or -/// which events were subscribed to, during a test session. Monitoring is session-based; begin a session with the Run -/// method and dispose the returned scope to finalize monitoring. +/// which events were subscribed to, during a test session.
+/// Monitoring is session-based: begin a session with the method and +/// dispose the returned scope to finalize monitoring. ///
public sealed class MockMonitor : MockMonitor { - /// - public MockMonitor(Mock mock) : base(mock.Interactions) - { - Verify = new Mock(mock.Subject, - new MockRegistration(mock.Registrations.Behavior, mock.Registrations.Prefix, Interactions)); - } - /// public MockMonitor(T mock) : this(mock as IMockSubject) { } private MockMonitor(IMockSubject? mockSubject) - : this(mockSubject?.Mock ?? throw new MockException("The subject is no mock.")) + : base(mockSubject?.Mock.Interactions ?? throw new MockException("The subject is no mock.")) { + Verify = new Mock(mockSubject.Mock.Subject, + new MockRegistration(mockSubject.Registrations.Behavior, mockSubject.Registrations.Prefix, Interactions)); } /// diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt index b3509efc..627319b5 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt @@ -568,7 +568,6 @@ namespace Mockolate.Monitor } public sealed class MockMonitor : Mockolate.Monitor.MockMonitor { - public MockMonitor(Mockolate.Mock mock) { } public MockMonitor(T mock) { } public Mockolate.Verify.IMockVerify Verify { get; } } diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt index cd26a7bf..1e5e8bf8 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt @@ -567,7 +567,6 @@ namespace Mockolate.Monitor } public sealed class MockMonitor : Mockolate.Monitor.MockMonitor { - public MockMonitor(Mockolate.Mock mock) { } public MockMonitor(T mock) { } public Mockolate.Verify.IMockVerify Verify { get; } } diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt index f1a1c10b..b9a169bb 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt @@ -534,7 +534,6 @@ namespace Mockolate.Monitor } public sealed class MockMonitor : Mockolate.Monitor.MockMonitor { - public MockMonitor(Mockolate.Mock mock) { } public MockMonitor(T mock) { } public Mockolate.Verify.IMockVerify Verify { get; } } diff --git a/Tests/Mockolate.Tests/Monitor/MockMonitorTests.cs b/Tests/Mockolate.Tests/Monitor/MockMonitorTests.cs index 343137df..85ba828f 100644 --- a/Tests/Mockolate.Tests/Monitor/MockMonitorTests.cs +++ b/Tests/Mockolate.Tests/Monitor/MockMonitorTests.cs @@ -1,3 +1,4 @@ +using Mockolate.Exceptions; using Mockolate.Monitor; using Mockolate.Tests.TestHelpers; @@ -19,6 +20,20 @@ public async Task ClearAllInteractions_WhenMonitorIsRunning_ShouldClearInternalC await That(monitor.Verify.Invoked.IsValid(It.Is(1))).Never(); } + [Fact] + public async Task Constructor_WithoutMock_ShouldThrowMockException() + { + MyServiceBase sut = new(); + + void Act() + { + _ = new MockMonitor(sut); + } + + await That(Act).Throws() + .WithMessage("The subject is no mock."); + } + [Fact] public async Task DisposeTwice_ShouldNotIncludeMoreInvocations() {