From 5013f6d392eebad4d7012c7307b4906e1ff49d5e Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:12:37 +0000 Subject: [PATCH 01/10] refactor(mocks): move control members from Mock to static Mock helpers Move 11 public instance members off Mock onto the Mock static class to eliminate naming collisions with user interface members. Mock now only exposes Object, Engine (hidden), and implicit conversion to T. IMock methods are implemented explicitly for MockRepository batch ops. --- TUnit.Mocks/Mock.cs | 103 ++++++++++++++++++++++++++++++++++++++++- TUnit.Mocks/MockOfT.cs | 88 +++-------------------------------- 2 files changed, 108 insertions(+), 83 deletions(-) diff --git a/TUnit.Mocks/Mock.cs b/TUnit.Mocks/Mock.cs index 2a8b23c6ce..a58d4f5da2 100644 --- a/TUnit.Mocks/Mock.cs +++ b/TUnit.Mocks/Mock.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using TUnit.Mocks.Diagnostics; using TUnit.Mocks.Verification; namespace TUnit.Mocks; @@ -82,7 +83,7 @@ public static void RegisterWrapFactory(Func> factory public static Mock Of(MockBehavior behavior, IDefaultValueProvider defaultValueProvider) where T : class { var mock = Of(behavior); - mock.DefaultValueProvider = defaultValueProvider; + mock.Engine.DefaultValueProvider = defaultValueProvider; return mock; } @@ -251,4 +252,104 @@ public static void VerifyInOrder(Action verificationActions) { OrderedVerification.Verify(verificationActions); } + + // ────────────────────────────────────────────────────────── + // Static helpers – expose control surface without cluttering Mock + // ────────────────────────────────────────────────────────── + + /// All calls made to this mock, in order. + public static IReadOnlyList Invocations(Mock mock) where T : class + => mock.Engine.GetAllCalls(); + + /// Returns the mock behavior (Loose or Strict). + public static MockBehavior GetBehavior(Mock mock) where T : class + => mock.Engine.Behavior; + + /// + /// Gets the custom default value provider for unconfigured methods in loose mode. + /// When set, this provider is consulted before auto-mocking and built-in defaults. + /// + public static IDefaultValueProvider? GetDefaultValueProvider(Mock mock) where T : class + => mock.Engine.DefaultValueProvider; + + /// + /// Sets the custom default value provider for unconfigured methods in loose mode. + /// When set, this provider is consulted before auto-mocking and built-in defaults. + /// + public static void SetDefaultValueProvider(Mock mock, IDefaultValueProvider? provider) where T : class + => mock.Engine.DefaultValueProvider = provider; + + /// + /// Enables auto-tracking for all properties. Property setters store values and getters return them, + /// acting like real auto-properties. Explicit setups take precedence over auto-tracked values. + /// + public static void SetupAllProperties(Mock mock) where T : class + => mock.Engine.AutoTrackProperties = true; + + /// Clears all setups and call history. + public static void Reset(Mock mock) where T : class + => ((IMock)mock).Reset(); + + /// + /// Verifies all registered setups were invoked at least once. + /// Throws listing uninvoked setups. + /// + public static void VerifyAll(Mock mock) where T : class + => ((IMock)mock).VerifyAll(); + + /// + /// Fails if any recorded call was not matched by a prior verification statement. + /// Throws listing unverified calls. + /// + public static void VerifyNoOtherCalls(Mock mock) where T : class + => ((IMock)mock).VerifyNoOtherCalls(); + + /// + /// Retrieves the auto-mock wrapper for a child interface returned by this mock. + /// Use this to configure auto-mocked return values. + /// + public static Mock GetAutoMock(Mock mock, string memberName) + where T : class + where TChild : class + { + var cacheKey = memberName + "|" + typeof(TChild).FullName; + if (mock.Engine.TryGetAutoMock(cacheKey, out var autoMock)) + { + return (Mock)autoMock; + } + + throw new InvalidOperationException( + $"No auto-mock found for member '{memberName}' returning type '{typeof(TChild).Name}'. " + + $"Ensure the method was called at least once before retrieving its auto-mock."); + } + + /// + /// Returns a diagnostic report of this mock's setup coverage and call matching. + /// + public static MockDiagnostics GetDiagnostics(Mock mock) where T : class + => mock.Engine.GetDiagnostics(); + + /// + /// Sets the current state for state machine mocking. Null clears the state. + /// + public static void SetState(Mock mock, string? stateName) where T : class + => mock.Engine.TransitionTo(stateName); + + /// + /// Configures setups scoped to a specific state. All setups registered inside + /// the action will only match when the engine is in the specified state. + /// + public static void InState(Mock mock, string stateName, Action> configure) where T : class + { + var previous = mock.Engine.PendingRequiredState; + mock.Engine.PendingRequiredState = stateName; + try + { + configure(mock); + } + finally + { + mock.Engine.PendingRequiredState = previous; + } + } } diff --git a/TUnit.Mocks/MockOfT.cs b/TUnit.Mocks/MockOfT.cs index e4ef159ffd..61c3f05f02 100644 --- a/TUnit.Mocks/MockOfT.cs +++ b/TUnit.Mocks/MockOfT.cs @@ -20,19 +20,6 @@ public class Mock : IMock where T : class /// object IMock.ObjectInstance => Object; - /// The mock behavior (Loose or Strict). - public MockBehavior Behavior => Engine.Behavior; - - /// - /// Gets or sets the custom default value provider for unconfigured methods in loose mode. - /// When set, this provider is consulted before auto-mocking and built-in defaults. - /// - public IDefaultValueProvider? DefaultValueProvider - { - get => Engine.DefaultValueProvider; - set => Engine.DefaultValueProvider = value; - } - /// Creates a Mock wrapping the given object and engine. Used by generated code. [EditorBrowsable(EditorBrowsableState.Never)] public Mock(T mockObject, MockEngine engine) @@ -41,26 +28,8 @@ public Mock(T mockObject, MockEngine engine) Object = mockObject; } - /// All calls made to this mock, in order. - public IReadOnlyList Invocations => Engine.GetAllCalls(); - - /// - /// Enables auto-tracking for all properties. Property setters store values and getters return them, - /// acting like real auto-properties. Explicit setups take precedence over auto-tracked values. - /// - public void SetupAllProperties() - { - Engine.AutoTrackProperties = true; - } - - /// Clears all setups and call history. - public void Reset() => Engine.Reset(); - - /// - /// Verifies all registered setups were invoked at least once. - /// Throws listing uninvoked setups. - /// - public void VerifyAll() + /// + void IMock.VerifyAll() { var setups = Engine.GetSetups(); var uninvoked = new List(); @@ -84,11 +53,8 @@ public void VerifyAll() } } - /// - /// Fails if any recorded call was not matched by a prior verification statement. - /// Throws listing unverified calls. - /// - public void VerifyNoOtherCalls() + /// + void IMock.VerifyNoOtherCalls() { var unverified = Engine.GetUnverifiedCalls(); if (unverified.Count > 0) @@ -99,50 +65,8 @@ public void VerifyNoOtherCalls() } } - /// - /// Retrieves the auto-mock wrapper for a child interface returned by this mock. - /// Use this to configure auto-mocked return values. - /// - public Mock GetAutoMock(string memberName) where TChild : class - { - var cacheKey = memberName + "|" + typeof(TChild).FullName; - if (Engine.TryGetAutoMock(cacheKey, out var mock)) - { - return (Mock)mock; - } - - throw new InvalidOperationException( - $"No auto-mock found for member '{memberName}' returning type '{typeof(TChild).Name}'. " + - $"Ensure the method was called at least once before retrieving its auto-mock."); - } - - /// - /// Returns a diagnostic report of this mock's setup coverage and call matching. - /// - public Diagnostics.MockDiagnostics GetDiagnostics() => Engine.GetDiagnostics(); - - /// - /// Sets the current state for state machine mocking. Null clears the state. - /// - public void SetState(string? stateName) => Engine.TransitionTo(stateName); - - /// - /// Configures setups scoped to a specific state. All setups registered inside - /// the action will only match when the engine is in the specified state. - /// - public void InState(string stateName, Action> configure) - { - var previous = Engine.PendingRequiredState; - Engine.PendingRequiredState = stateName; - try - { - configure(this); - } - finally - { - Engine.PendingRequiredState = previous; - } - } + /// + void IMock.Reset() => Engine.Reset(); /// Implicit conversion to T -- no .Object needed. public static implicit operator T(Mock mock) => mock.Object; From 27fc21f95a16bad64a3d7021e7589dc39443e5f3 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:12:56 +0000 Subject: [PATCH 02/10] refactor(mocks): shrink MockMemberNames to Object/Engine + System.Object members Only Object, Engine, and inherited System.Object members need reservation now that all control members live on the static Mock class. --- TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs index 362b94ef9b..4dd6587d8d 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs @@ -16,9 +16,7 @@ internal static class MockMembersBuilder private static readonly HashSet MockMemberNames = new(System.StringComparer.Ordinal) { - "Object", "Engine", "Behavior", "Invocations", "DefaultValueProvider", - "SetupAllProperties", "Reset", "VerifyAll", "VerifyNoOtherCalls", - "GetAutoMock", "GetDiagnostics", "SetState", "InState", + "Object", "Engine", "GetHashCode", "GetType", "ToString", "Equals" }; From c211e23355b5e4b7a66f03787b19959966b1a420 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:13:43 +0000 Subject: [PATCH 03/10] test(mocks): update snapshot for reduced MockMemberNames set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reset_ → Reset now that Reset is no longer a reserved Mock member name. --- .../Snapshots/Multi_Method_Interface.verified.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Method_Interface.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Method_Interface.verified.txt index e821bafd8c..12489b560b 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Method_Interface.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Method_Interface.verified.txt @@ -84,7 +84,7 @@ namespace TUnit.Mocks.Generated return new ICalculator_Subtract_M1_MockCall(mock.Engine, 1, "Subtract", matchers); } - public static global::TUnit.Mocks.VoidMockMethodCall Reset_(this global::TUnit.Mocks.Mock mock) + public static global::TUnit.Mocks.VoidMockMethodCall Reset(this global::TUnit.Mocks.Mock mock) { var matchers = global::System.Array.Empty(); return new global::TUnit.Mocks.VoidMockMethodCall(mock.Engine, 2, "Reset", matchers); From 9fe56fdd063cee898dd3dbf3aaecf2d50baf4044 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:20:00 +0000 Subject: [PATCH 04/10] test(mocks): update all tests to use static Mock helper API Migrate ~100 call sites across 19 test files from instance methods (mock.VerifyAll(), mock.Reset(), etc.) to static helpers (Mock.VerifyAll(mock), Mock.Reset(mock), etc.). --- TUnit.Mocks.Tests/AdditionalCoverageTests.cs | 38 ++++++------ TUnit.Mocks.Tests/AutoMockTests.cs | 2 +- TUnit.Mocks.Tests/AutoTrackPropertyTests.cs | 16 ++--- TUnit.Mocks.Tests/BasicMockTests.cs | 2 +- .../DefaultValueProviderTests.cs | 8 +-- TUnit.Mocks.Tests/DiagnosticsTests.cs | 16 ++--- TUnit.Mocks.Tests/EdgeCaseTests.cs | 4 +- .../EventSubscriptionSetupTests.cs | 2 +- .../EventSubscriptionVerifyTests.cs | 2 +- TUnit.Mocks.Tests/InvocationsTests.cs | 14 ++--- TUnit.Mocks.Tests/MockRepositoryTests.cs | 12 ++-- TUnit.Mocks.Tests/MultipleInterfaceTests.cs | 6 +- TUnit.Mocks.Tests/OrderedVerificationTests.cs | 4 +- TUnit.Mocks.Tests/ProtectedMemberTests.cs | 2 +- TUnit.Mocks.Tests/ResetTests.cs | 22 +++---- TUnit.Mocks.Tests/StateMachineTests.cs | 60 +++++++++---------- TUnit.Mocks.Tests/StrictModeTests.cs | 4 +- TUnit.Mocks.Tests/VerifyAllTests.cs | 10 ++-- TUnit.Mocks.Tests/VerifyNoOtherCallsTests.cs | 12 ++-- 19 files changed, 118 insertions(+), 118 deletions(-) diff --git a/TUnit.Mocks.Tests/AdditionalCoverageTests.cs b/TUnit.Mocks.Tests/AdditionalCoverageTests.cs index c0c33427ac..d0b9608cff 100644 --- a/TUnit.Mocks.Tests/AdditionalCoverageTests.cs +++ b/TUnit.Mocks.Tests/AdditionalCoverageTests.cs @@ -190,7 +190,7 @@ public async Task GetAutoMock_Throws_When_No_AutoMock_Exists() // No method has been called, so no auto-mock was created var ex = Assert.Throws(() => { - mock.GetAutoMock("NonExistentMethod"); + Mock.GetAutoMock(mock, "NonExistentMethod"); }); await Assert.That(ex.Message).Contains("No auto-mock found"); @@ -212,7 +212,7 @@ public async Task VerifyAll_Message_Includes_Matcher_Descriptions() // Act — don't call the method // Assert - var ex = Assert.Throws(() => mock.VerifyAll()); + var ex = Assert.Throws(() => Mock.VerifyAll(mock)); await Assert.That(ex.Message).Contains("Add("); await Assert.That(ex.Message).Contains("never invoked"); } @@ -229,7 +229,7 @@ public async Task VerifyAll_Message_Lists_Multiple_Uninvoked_Setups() // Act — don't call any methods // Assert - var ex = Assert.Throws(() => mock.VerifyAll()); + var ex = Assert.Throws(() => Mock.VerifyAll(mock)); await Assert.That(ex.Message).Contains("Add("); await Assert.That(ex.Message).Contains("GetName()"); await Assert.That(ex.Message).Contains("Log("); @@ -248,7 +248,7 @@ public async Task VerifyAll_Passes_When_All_Setups_Invoked() mock.Object.GetName(); // Assert — no exception - mock.VerifyAll(); + Mock.VerifyAll(mock); await Assert.That(true).IsTrue(); } } @@ -267,11 +267,11 @@ public async Task Invocations_Are_In_Call_Order() mock.Object.GetName(); mock.Object.Add(3, 4); - await Assert.That(mock.Invocations).HasCount().EqualTo(4); - await Assert.That(mock.Invocations[0].MemberName).IsEqualTo("Add"); - await Assert.That(mock.Invocations[1].MemberName).IsEqualTo("Log"); - await Assert.That(mock.Invocations[2].MemberName).IsEqualTo("GetName"); - await Assert.That(mock.Invocations[3].MemberName).IsEqualTo("Add"); + await Assert.That(Mock.Invocations(mock)).HasCount().EqualTo(4); + await Assert.That(Mock.Invocations(mock)[0].MemberName).IsEqualTo("Add"); + await Assert.That(Mock.Invocations(mock)[1].MemberName).IsEqualTo("Log"); + await Assert.That(Mock.Invocations(mock)[2].MemberName).IsEqualTo("GetName"); + await Assert.That(Mock.Invocations(mock)[3].MemberName).IsEqualTo("Add"); } [Test] @@ -283,10 +283,10 @@ public async Task Invocations_Sequence_Numbers_Are_Monotonically_Increasing() mock.Object.GetName(); mock.Object.Log("msg"); - for (int i = 1; i < mock.Invocations.Count; i++) + for (int i = 1; i < Mock.Invocations(mock).Count; i++) { - await Assert.That(mock.Invocations[i].SequenceNumber) - .IsGreaterThan(mock.Invocations[i - 1].SequenceNumber); + await Assert.That(Mock.Invocations(mock)[i].SequenceNumber) + .IsGreaterThan(Mock.Invocations(mock)[i - 1].SequenceNumber); } } } @@ -305,7 +305,7 @@ public class AutoTrackPropertyResetTests public async Task Reset_Clears_Auto_Tracked_Property_Values() { var mock = Mock.Of(); - mock.SetupAllProperties(); + Mock.SetupAllProperties(mock); var svc = mock.Object; svc.Theme = "dark"; @@ -315,8 +315,8 @@ public async Task Reset_Clears_Auto_Tracked_Property_Values() await Assert.That(svc.FontSize).IsEqualTo(14); // Reset should clear tracked values - mock.Reset(); - mock.SetupAllProperties(); + Mock.Reset(mock); + Mock.SetupAllProperties(mock); // After reset + re-enable auto-track, values are back to defaults await Assert.That(svc.Theme).IsNotEqualTo("dark"); @@ -373,9 +373,9 @@ public async Task DefaultValueProvider_Get_Set_Roundtrip() var mock = Mock.Of(); var provider = new FixedStringProvider(); - mock.DefaultValueProvider = provider; + Mock.SetDefaultValueProvider(mock, provider); - await Assert.That(mock.DefaultValueProvider).IsEqualTo(provider); + await Assert.That(Mock.GetDefaultValueProvider(mock)).IsEqualTo(provider); } [Test] @@ -398,13 +398,13 @@ public class MockBehaviorPropertyTests public async Task Loose_Mock_Has_Loose_Behavior() { var mock = Mock.Of(); - await Assert.That(mock.Behavior).IsEqualTo(MockBehavior.Loose); + await Assert.That(Mock.GetBehavior(mock)).IsEqualTo(MockBehavior.Loose); } [Test] public async Task Strict_Mock_Has_Strict_Behavior() { var mock = Mock.Of(MockBehavior.Strict); - await Assert.That(mock.Behavior).IsEqualTo(MockBehavior.Strict); + await Assert.That(Mock.GetBehavior(mock)).IsEqualTo(MockBehavior.Strict); } } diff --git a/TUnit.Mocks.Tests/AutoMockTests.cs b/TUnit.Mocks.Tests/AutoMockTests.cs index 2908f18212..675d12fe09 100644 --- a/TUnit.Mocks.Tests/AutoMockTests.cs +++ b/TUnit.Mocks.Tests/AutoMockTests.cs @@ -131,7 +131,7 @@ public async Task Auto_Mock_Configurable_Via_GetAutoMock() var serviceB = mock.Object.GetServiceB(); // Retrieve and configure the auto-mock - var autoMock = mock.GetAutoMock("GetServiceB"); + var autoMock = Mock.GetAutoMock(mock, "GetServiceB"); autoMock.GetValue().Returns(42); // Act diff --git a/TUnit.Mocks.Tests/AutoTrackPropertyTests.cs b/TUnit.Mocks.Tests/AutoTrackPropertyTests.cs index 543f1d203a..d2be1da16a 100644 --- a/TUnit.Mocks.Tests/AutoTrackPropertyTests.cs +++ b/TUnit.Mocks.Tests/AutoTrackPropertyTests.cs @@ -23,7 +23,7 @@ public async Task AutoTrack_Set_Then_Get_Returns_Value() { // Arrange — opt in to auto-tracking var mock = Mock.Of(); - mock.SetupAllProperties(); + Mock.SetupAllProperties(mock); // Act mock.Object.Name = "Alice"; @@ -38,7 +38,7 @@ public async Task AutoTrack_Multiple_Properties_Track_Independently() { // Arrange var mock = Mock.Of(); - mock.SetupAllProperties(); + Mock.SetupAllProperties(mock); // Act mock.Object.Name = "Bob"; @@ -68,7 +68,7 @@ public async Task Explicit_Setup_Overrides_AutoTrack() { // Arrange var mock = Mock.Of(); - mock.SetupAllProperties(); + Mock.SetupAllProperties(mock); mock.Name.Returns("Configured"); // Act — set a tracked value, but explicit setup should win @@ -83,7 +83,7 @@ public async Task AutoTrack_Overwrite_Value() { // Arrange var mock = Mock.Of(); - mock.SetupAllProperties(); + Mock.SetupAllProperties(mock); // Act — set then overwrite mock.Object.Name = "First"; @@ -111,7 +111,7 @@ public async Task Loose_Mode_AutoTracks_After_SetupAllProperties() { // Arrange — explicit opt-in enables auto-tracking var mock = Mock.Of(); - mock.SetupAllProperties(); + Mock.SetupAllProperties(mock); // Act mock.Object.Name = "Alice"; @@ -140,7 +140,7 @@ public async Task Strict_Mode_With_SetupAllProperties_Tracks() { // Arrange — strict mode with explicit opt-in var mock = Mock.Of(MockBehavior.Strict); - mock.SetupAllProperties(); + Mock.SetupAllProperties(mock); mock.Name.Set(Arg.Any()); mock.Name.Returns(""); @@ -156,11 +156,11 @@ public async Task Reset_Clears_Tracked_Values_But_Keeps_AutoTrack() { // Arrange var mock = Mock.Of(); - mock.SetupAllProperties(); + Mock.SetupAllProperties(mock); mock.Object.Name = "Alice"; // Act - mock.Reset(); + Mock.Reset(mock); // Assert — tracked values cleared, but auto-track still active await Assert.That(mock.Object.Name).IsEmpty(); diff --git a/TUnit.Mocks.Tests/BasicMockTests.cs b/TUnit.Mocks.Tests/BasicMockTests.cs index e28facf8d0..8a07da3478 100644 --- a/TUnit.Mocks.Tests/BasicMockTests.cs +++ b/TUnit.Mocks.Tests/BasicMockTests.cs @@ -160,7 +160,7 @@ public async Task Reset_Clears_Setups() await Assert.That(calc.Add(1, 1)).IsEqualTo(42); // Act - mock.Reset(); + Mock.Reset(mock); // Assert — after reset, returns default await Assert.That(calc.Add(1, 1)).IsEqualTo(0); diff --git a/TUnit.Mocks.Tests/DefaultValueProviderTests.cs b/TUnit.Mocks.Tests/DefaultValueProviderTests.cs index fe16f34702..58adcd6b08 100644 --- a/TUnit.Mocks.Tests/DefaultValueProviderTests.cs +++ b/TUnit.Mocks.Tests/DefaultValueProviderTests.cs @@ -28,7 +28,7 @@ public async Task Custom_Provider_Returns_String_Default() { // Arrange var mock = Mock.Of(); - mock.DefaultValueProvider = new CustomProvider(); + Mock.SetDefaultValueProvider(mock, new CustomProvider()); IGreeter greeter = mock.Object; @@ -44,7 +44,7 @@ public async Task Custom_Provider_Returns_Int_Default() { // Arrange var mock = Mock.Of(); - mock.DefaultValueProvider = new CustomProvider(); + Mock.SetDefaultValueProvider(mock, new CustomProvider()); ICalculator calc = mock.Object; @@ -60,7 +60,7 @@ public async Task Setup_Takes_Precedence_Over_Provider() { // Arrange var mock = Mock.Of(); - mock.DefaultValueProvider = new CustomProvider(); + Mock.SetDefaultValueProvider(mock, new CustomProvider()); mock.Add(1, 2).Returns(100); ICalculator calc = mock.Object; @@ -92,7 +92,7 @@ public async Task BuiltIn_Provider_Returns_Empty_String() { // Arrange var mock = Mock.Of(); - mock.DefaultValueProvider = DefaultValueProvider.Instance; + Mock.SetDefaultValueProvider(mock, DefaultValueProvider.Instance); IGreeter greeter = mock.Object; diff --git a/TUnit.Mocks.Tests/DiagnosticsTests.cs b/TUnit.Mocks.Tests/DiagnosticsTests.cs index aaec7c574b..b9d98a6f16 100644 --- a/TUnit.Mocks.Tests/DiagnosticsTests.cs +++ b/TUnit.Mocks.Tests/DiagnosticsTests.cs @@ -16,7 +16,7 @@ public async Task Unused_Setups_Detected() ICalculator calc = mock.Object; _ = calc.Add(1, 2); // only exercise the first setup - var diag = mock.GetDiagnostics(); + var diag = Mock.GetDiagnostics(mock); await Assert.That(diag.TotalSetups).IsEqualTo(2); await Assert.That(diag.ExercisedSetups).IsEqualTo(1); @@ -36,7 +36,7 @@ public async Task Unmatched_Calls_Detected() _ = calc.Add(1, 2); // matches setup _ = calc.Add(5, 5); // no setup match — unmatched - var diag = mock.GetDiagnostics(); + var diag = Mock.GetDiagnostics(mock); await Assert.That(diag.UnmatchedCalls).HasCount().EqualTo(1); await Assert.That(diag.UnmatchedCalls[0].MemberName).IsEqualTo("Add"); @@ -52,7 +52,7 @@ public async Task All_Setups_Exercised() ICalculator calc = mock.Object; _ = calc.Add(1, 2); - var diag = mock.GetDiagnostics(); + var diag = Mock.GetDiagnostics(mock); await Assert.That(diag.TotalSetups).IsEqualTo(1); await Assert.That(diag.ExercisedSetups).IsEqualTo(1); @@ -67,7 +67,7 @@ public async Task No_Calls_Means_All_Setups_Unused() mock.Add(1, 2).Returns(3); mock.Add(3, 4).Returns(7); - var diag = mock.GetDiagnostics(); + var diag = Mock.GetDiagnostics(mock); await Assert.That(diag.TotalSetups).IsEqualTo(2); await Assert.That(diag.ExercisedSetups).IsEqualTo(0); @@ -80,7 +80,7 @@ public async Task Matcher_Descriptions_Populated() var mock = Mock.Of(); mock.Add(Arg.Any(), Arg.Is(x => x > 0)).Returns(1); - var diag = mock.GetDiagnostics(); + var diag = Mock.GetDiagnostics(mock); await Assert.That(diag.UnusedSetups).HasCount().EqualTo(1); var setup = diag.UnusedSetups[0]; @@ -98,9 +98,9 @@ public async Task Reset_Clears_Diagnostics() ICalculator calc = mock.Object; _ = calc.Add(5, 5); // unmatched - mock.Reset(); + Mock.Reset(mock); - var diag = mock.GetDiagnostics(); + var diag = Mock.GetDiagnostics(mock); await Assert.That(diag.TotalSetups).IsEqualTo(0); await Assert.That(diag.ExercisedSetups).IsEqualTo(0); @@ -113,7 +113,7 @@ public async Task Empty_Mock_Has_Clean_Diagnostics() { var mock = Mock.Of(); - var diag = mock.GetDiagnostics(); + var diag = Mock.GetDiagnostics(mock); await Assert.That(diag.TotalSetups).IsEqualTo(0); await Assert.That(diag.ExercisedSetups).IsEqualTo(0); diff --git a/TUnit.Mocks.Tests/EdgeCaseTests.cs b/TUnit.Mocks.Tests/EdgeCaseTests.cs index bf1eca5076..651a718ae0 100644 --- a/TUnit.Mocks.Tests/EdgeCaseTests.cs +++ b/TUnit.Mocks.Tests/EdgeCaseTests.cs @@ -582,7 +582,7 @@ public async Task Reset_Then_Reconfigure_Different_Behavior() await Assert.That(firstResult).IsEqualTo("A"); // Reset and reconfigure - mock.Reset(); + Mock.Reset(mock); mock.GetConfig("key").Returns("B"); // Second phase @@ -609,7 +609,7 @@ public async Task Reset_Mid_Test_Changes_Verification_Baseline() mock.GetConfig("key2").WasCalled(Times.Once); // Reset — clears call history - mock.Reset(); + Mock.Reset(mock); // Post-reset: previous calls should not count mock.GetConfig("key1").WasNeverCalled(); diff --git a/TUnit.Mocks.Tests/EventSubscriptionSetupTests.cs b/TUnit.Mocks.Tests/EventSubscriptionSetupTests.cs index 3f2c6835d6..4099cd0c66 100644 --- a/TUnit.Mocks.Tests/EventSubscriptionSetupTests.cs +++ b/TUnit.Mocks.Tests/EventSubscriptionSetupTests.cs @@ -87,7 +87,7 @@ public async Task Reset_Clears_Subscription_Callbacks() var callbackFired = false; mock.Events.DataReady.OnSubscribe(() => callbackFired = true); - mock.Reset(); + Mock.Reset(mock); mock.Object.DataReady += (sender, args) => { }; diff --git a/TUnit.Mocks.Tests/EventSubscriptionVerifyTests.cs b/TUnit.Mocks.Tests/EventSubscriptionVerifyTests.cs index 3ff007ccb7..9d2332b9ef 100644 --- a/TUnit.Mocks.Tests/EventSubscriptionVerifyTests.cs +++ b/TUnit.Mocks.Tests/EventSubscriptionVerifyTests.cs @@ -83,7 +83,7 @@ public async Task Reset_Clears_Subscription_History() mock.Object.OnStringAction += _ => { }; // Act - mock.Reset(); + Mock.Reset(mock); // Assert await Assert.That(mock.Events.OnStringAction.WasSubscribed).IsFalse(); diff --git a/TUnit.Mocks.Tests/InvocationsTests.cs b/TUnit.Mocks.Tests/InvocationsTests.cs index f399c6311b..155cec0b79 100644 --- a/TUnit.Mocks.Tests/InvocationsTests.cs +++ b/TUnit.Mocks.Tests/InvocationsTests.cs @@ -19,7 +19,7 @@ public async Task Invocations_Returns_All_Calls() svc.GetValue("key2"); svc.Process(42); - await Assert.That(mock.Invocations.Count).IsEqualTo(3); + await Assert.That(Mock.Invocations(mock).Count).IsEqualTo(3); } [Test] @@ -32,8 +32,8 @@ public async Task Invocations_Contains_Correct_Method_Names() svc.GetValue("key1"); svc.Process(99); - await Assert.That(mock.Invocations[0].MemberName).IsEqualTo("GetValue"); - await Assert.That(mock.Invocations[1].MemberName).IsEqualTo("Process"); + await Assert.That(Mock.Invocations(mock)[0].MemberName).IsEqualTo("GetValue"); + await Assert.That(Mock.Invocations(mock)[1].MemberName).IsEqualTo("Process"); } [Test] @@ -45,14 +45,14 @@ public async Task Invocations_Contains_Correct_Arguments() var svc = mock.Object; svc.GetValue("hello"); - await Assert.That(mock.Invocations[0].Arguments[0]).IsEqualTo("hello"); + await Assert.That(Mock.Invocations(mock)[0].Arguments[0]).IsEqualTo("hello"); } [Test] public async Task Invocations_Is_Empty_When_No_Calls_Made() { var mock = Mock.Of(); - await Assert.That(mock.Invocations.Count).IsEqualTo(0); + await Assert.That(Mock.Invocations(mock).Count).IsEqualTo(0); } [Test] @@ -64,8 +64,8 @@ public async Task Invocations_Is_Empty_After_Reset() var svc = mock.Object; svc.GetValue("key1"); - mock.Reset(); + Mock.Reset(mock); - await Assert.That(mock.Invocations.Count).IsEqualTo(0); + await Assert.That(Mock.Invocations(mock).Count).IsEqualTo(0); } } diff --git a/TUnit.Mocks.Tests/MockRepositoryTests.cs b/TUnit.Mocks.Tests/MockRepositoryTests.cs index 61b238edd4..92d4f3c51d 100644 --- a/TUnit.Mocks.Tests/MockRepositoryTests.cs +++ b/TUnit.Mocks.Tests/MockRepositoryTests.cs @@ -137,8 +137,8 @@ public async Task Repository_Reset_Clears_All_Mocks() // Assert — setups and history are cleared await Assert.That(serviceMock.Object.GetData(1)).IsEmpty(); // no setup, returns smart default - await Assert.That(serviceMock.Invocations).Count().IsEqualTo(1); // only the new call - await Assert.That(loggerMock.Invocations).Count().IsEqualTo(0); // history cleared + await Assert.That(Mock.Invocations(serviceMock)).Count().IsEqualTo(1); // only the new call + await Assert.That(Mock.Invocations(loggerMock)).Count().IsEqualTo(0); // history cleared } [Test] @@ -149,7 +149,7 @@ public async Task Repository_Uses_Default_Behavior() var mock = repo.Of(); // Assert — mock inherits strict behavior - await Assert.That(mock.Behavior).IsEqualTo(MockBehavior.Strict); + await Assert.That(Mock.GetBehavior(mock)).IsEqualTo(MockBehavior.Strict); } [Test] @@ -160,7 +160,7 @@ public async Task Repository_Behavior_Can_Be_Overridden_Per_Mock() var looseMock = repo.Of(MockBehavior.Loose); // Assert — specific behavior overrides repository default - await Assert.That(looseMock.Behavior).IsEqualTo(MockBehavior.Loose); + await Assert.That(Mock.GetBehavior(looseMock)).IsEqualTo(MockBehavior.Loose); } [Test] @@ -274,7 +274,7 @@ public async Task Repository_OfPartial_With_Strict_Behavior() var mock = repo.OfPartial(); // Assert — partial mock inherits strict behavior from repository - await Assert.That(mock.Behavior).IsEqualTo(MockBehavior.Strict); + await Assert.That(Mock.GetBehavior(mock)).IsEqualTo(MockBehavior.Strict); } [Test] @@ -285,6 +285,6 @@ public async Task Repository_OfPartial_Behavior_Override() var mock = repo.OfPartial(MockBehavior.Loose); // Assert — behavior overridden - await Assert.That(mock.Behavior).IsEqualTo(MockBehavior.Loose); + await Assert.That(Mock.GetBehavior(mock)).IsEqualTo(MockBehavior.Loose); } } diff --git a/TUnit.Mocks.Tests/MultipleInterfaceTests.cs b/TUnit.Mocks.Tests/MultipleInterfaceTests.cs index 06894335c5..d994446bc8 100644 --- a/TUnit.Mocks.Tests/MultipleInterfaceTests.cs +++ b/TUnit.Mocks.Tests/MultipleInterfaceTests.cs @@ -114,7 +114,7 @@ public async Task Multi_Mock_Shares_Single_Engine() ((IMultiDisposable)mock.Object).Dispose(); // Assert — all calls recorded in invocations - await Assert.That(mock.Invocations).Count().IsEqualTo(2); + await Assert.That(Mock.Invocations(mock)).Count().IsEqualTo(2); } [Test] @@ -180,7 +180,7 @@ public async Task Mock_Of_Four_Interfaces_Tracks_All_Calls() ((IMultiCloneable)mock.Object).Clone(); // Assert — all 4 calls tracked - await Assert.That(mock.Invocations).Count().IsEqualTo(4); + await Assert.That(Mock.Invocations(mock)).Count().IsEqualTo(4); } [Test] @@ -190,6 +190,6 @@ public async Task Mock_Of_Two_Interfaces_With_Strict_Behavior() var mock = Mock.Of(MockBehavior.Strict); // Assert — strict behavior inherited - await Assert.That(mock.Behavior).IsEqualTo(MockBehavior.Strict); + await Assert.That(Mock.GetBehavior(mock)).IsEqualTo(MockBehavior.Strict); } } diff --git a/TUnit.Mocks.Tests/OrderedVerificationTests.cs b/TUnit.Mocks.Tests/OrderedVerificationTests.cs index 7db7410248..2fba4d2305 100644 --- a/TUnit.Mocks.Tests/OrderedVerificationTests.cs +++ b/TUnit.Mocks.Tests/OrderedVerificationTests.cs @@ -340,7 +340,7 @@ public void VerifyInOrder_Marks_Calls_As_Verified_For_VerifyNoOtherCalls() }); // This should pass because the calls above were verified in VerifyInOrder - mock.VerifyNoOtherCalls(); + Mock.VerifyNoOtherCalls(mock); } [Test] @@ -365,7 +365,7 @@ public async Task VerifyInOrder_Partial_Verification_Leaves_Unverified_Calls() // Log("hello") was not verified, so this should fail var exception = Assert.Throws(() => { - mock.VerifyNoOtherCalls(); + Mock.VerifyNoOtherCalls(mock); }); await Assert.That(exception.Message).Contains("Log(hello)"); diff --git a/TUnit.Mocks.Tests/ProtectedMemberTests.cs b/TUnit.Mocks.Tests/ProtectedMemberTests.cs index bfa0833fa9..3ecbfd89e7 100644 --- a/TUnit.Mocks.Tests/ProtectedMemberTests.cs +++ b/TUnit.Mocks.Tests/ProtectedMemberTests.cs @@ -88,7 +88,7 @@ public async Task Protected_Method_Calls_Are_Recorded_In_Invocations() mock.Object.ProcessAndFormat(7); // Assert — both ComputeValue (base call) and FormatResult are recorded - await Assert.That(mock.Invocations).Count().IsGreaterThanOrEqualTo(2); + await Assert.That(Mock.Invocations(mock)).Count().IsGreaterThanOrEqualTo(2); } [Test] diff --git a/TUnit.Mocks.Tests/ResetTests.cs b/TUnit.Mocks.Tests/ResetTests.cs index 008c5f2e90..470c065404 100644 --- a/TUnit.Mocks.Tests/ResetTests.cs +++ b/TUnit.Mocks.Tests/ResetTests.cs @@ -23,7 +23,7 @@ public async Task Reset_Clears_All_Setups() await Assert.That(calc.Add(3, 4)).IsEqualTo(99); // Act - mock.Reset(); + Mock.Reset(mock); // Assert — after reset, all setups are gone, returns default await Assert.That(calc.Add(1, 2)).IsEqualTo(0); @@ -44,7 +44,7 @@ public async Task Reset_Clears_Call_History() mock.Add(Arg.Any(), Arg.Any()).WasCalled(Times.Exactly(3)); // Act - mock.Reset(); + Mock.Reset(mock); // Assert — after reset, call history is cleared mock.Add(Arg.Any(), Arg.Any()).WasNeverCalled(); @@ -62,7 +62,7 @@ public async Task Reset_Allows_Fresh_Setup() await Assert.That(calc.Add(1, 2)).IsEqualTo(42); // Act — reset and reconfigure with different return value - mock.Reset(); + Mock.Reset(mock); mock.Add(1, 2).Returns(100); // Assert — new setup is in effect @@ -81,7 +81,7 @@ public async Task Reset_Allows_Fresh_Verification() mock.Add(1, 2).WasCalled(Times.Exactly(2)); // Act - mock.Reset(); + Mock.Reset(mock); // Make one new call calc.Add(1, 2); @@ -102,7 +102,7 @@ public async Task Reset_On_Strict_Mock_Restores_Strict_Behavior() await Assert.That(calc.Add(1, 2)).IsEqualTo(3); // Act — reset clears all setups - mock.Reset(); + Mock.Reset(mock); // Assert — strict mock throws again for unconfigured calls var exception = Assert.Throws(() => @@ -125,7 +125,7 @@ public async Task Reset_Clears_String_Method_Setup() await Assert.That(greeter.Greet("Alice")).IsEqualTo("Hello, Alice!"); // Act - mock.Reset(); + Mock.Reset(mock); // Assert — after reset, returns default (empty string for non-nullable string) var result = greeter.Greet("Alice"); @@ -144,7 +144,7 @@ public async Task Reset_Clears_Void_Method_Call_History() mock.Log(Arg.Any()).WasCalled(Times.Exactly(2)); // Act - mock.Reset(); + Mock.Reset(mock); // Assert — void method call history is cleared mock.Log(Arg.Any()).WasNeverCalled(); @@ -163,7 +163,7 @@ public async Task Reset_Followed_By_New_Setup_And_Verification() mock.Add(1, 1).WasCalled(Times.Once); // Act — reset - mock.Reset(); + Mock.Reset(mock); // Re-setup with new values mock.Add(1, 1).Returns(20); @@ -188,17 +188,17 @@ public async Task Multiple_Resets() // First cycle mock.Add(1, 1).Returns(10); await Assert.That(calc.Add(1, 1)).IsEqualTo(10); - mock.Reset(); + Mock.Reset(mock); // Second cycle mock.Add(1, 1).Returns(20); await Assert.That(calc.Add(1, 1)).IsEqualTo(20); - mock.Reset(); + Mock.Reset(mock); // Third cycle mock.Add(1, 1).Returns(30); await Assert.That(calc.Add(1, 1)).IsEqualTo(30); - mock.Reset(); + Mock.Reset(mock); // After final reset — returns default await Assert.That(calc.Add(1, 1)).IsEqualTo(0); diff --git a/TUnit.Mocks.Tests/StateMachineTests.cs b/TUnit.Mocks.Tests/StateMachineTests.cs index 8fe1d67182..6c5ed2e8c9 100644 --- a/TUnit.Mocks.Tests/StateMachineTests.cs +++ b/TUnit.Mocks.Tests/StateMachineTests.cs @@ -15,14 +15,14 @@ public class StateMachineTests public async Task State_Machine_Returns_Different_Values_Per_State() { var mock = Mock.Of(); - mock.SetState("disconnected"); + Mock.SetState(mock, "disconnected"); - mock.InState("disconnected", m => + Mock.InState(mock, "disconnected", m => { m.GetStatus().Returns("OFFLINE"); }); - mock.InState("connected", m => + Mock.InState(mock, "connected", m => { m.GetStatus().Returns("ONLINE"); }); @@ -31,10 +31,10 @@ public async Task State_Machine_Returns_Different_Values_Per_State() await Assert.That(conn.GetStatus()).IsEqualTo("OFFLINE"); - mock.SetState("connected"); + Mock.SetState(mock, "connected"); await Assert.That(conn.GetStatus()).IsEqualTo("ONLINE"); - mock.SetState("disconnected"); + Mock.SetState(mock, "disconnected"); await Assert.That(conn.GetStatus()).IsEqualTo("OFFLINE"); } @@ -42,15 +42,15 @@ public async Task State_Machine_Returns_Different_Values_Per_State() public async Task TransitionsTo_Changes_State_After_Call() { var mock = Mock.Of(); - mock.SetState("disconnected"); + Mock.SetState(mock, "disconnected"); - mock.InState("disconnected", m => + Mock.InState(mock, "disconnected", m => { m.Connect().TransitionsTo("connected"); m.GetStatus().Returns("OFFLINE"); }); - mock.InState("connected", m => + Mock.InState(mock, "connected", m => { m.Disconnect().TransitionsTo("disconnected"); m.GetStatus().Returns("ONLINE"); @@ -71,14 +71,14 @@ public async Task TransitionsTo_Changes_State_After_Call() public async Task State_Scoped_Throws() { var mock = Mock.Of(); - mock.SetState("connected"); + Mock.SetState(mock, "connected"); - mock.InState("connected", m => + Mock.InState(mock, "connected", m => { m.Connect().Throws(); }); - mock.InState("disconnected", m => + Mock.InState(mock, "disconnected", m => { m.Disconnect().Throws(); }); @@ -97,7 +97,7 @@ public async Task State_Scoped_Throws() public async Task No_State_Setups_Match_In_Any_State() { var mock = Mock.Of(); - mock.SetState("disconnected"); + Mock.SetState(mock, "disconnected"); // Setup without state guard — matches in any state mock.GetStatus().Returns("ALWAYS"); @@ -105,10 +105,10 @@ public async Task No_State_Setups_Match_In_Any_State() IConnection conn = mock.Object; await Assert.That(conn.GetStatus()).IsEqualTo("ALWAYS"); - mock.SetState("connected"); + Mock.SetState(mock, "connected"); await Assert.That(conn.GetStatus()).IsEqualTo("ALWAYS"); - mock.SetState(null); + Mock.SetState(mock, null); await Assert.That(conn.GetStatus()).IsEqualTo("ALWAYS"); } @@ -121,7 +121,7 @@ public async Task State_Scoped_Setup_Overrides_Global_When_In_State() mock.GetStatus().Returns("DEFAULT"); // State-scoped setup - mock.InState("special", m => + Mock.InState(mock, "special", m => { m.GetStatus().Returns("SPECIAL"); }); @@ -132,11 +132,11 @@ public async Task State_Scoped_Setup_Overrides_Global_When_In_State() await Assert.That(conn.GetStatus()).IsEqualTo("DEFAULT"); // State set — scoped setup wins (last-wins semantics, state-scoped was added later) - mock.SetState("special"); + Mock.SetState(mock, "special"); await Assert.That(conn.GetStatus()).IsEqualTo("SPECIAL"); // Different state — scoped doesn't match, global wins - mock.SetState("other"); + Mock.SetState(mock, "other"); await Assert.That(conn.GetStatus()).IsEqualTo("DEFAULT"); } @@ -144,9 +144,9 @@ public async Task State_Scoped_Setup_Overrides_Global_When_In_State() public async Task Strict_Mode_Throws_For_Unconfigured_Call_In_State() { var mock = Mock.Of(MockBehavior.Strict); - mock.SetState("disconnected"); + Mock.SetState(mock, "disconnected"); - mock.InState("disconnected", m => + Mock.InState(mock, "disconnected", m => { m.GetStatus().Returns("OFFLINE"); }); @@ -163,14 +163,14 @@ public async Task Nested_InState_Restores_Previous_State_Scope() { // Regression test: nested InState calls must save/restore PendingRequiredState var mock = Mock.Of(); - mock.SetState("outer"); + Mock.SetState(mock, "outer"); - mock.InState("outer", m => + Mock.InState(mock, "outer", m => { m.GetStatus().Returns("OUTER"); // Nested InState should temporarily switch to "inner" scope - mock.InState("inner", m => + Mock.InState(mock, "inner", m => { m.Connect(); }); @@ -186,7 +186,7 @@ public async Task Nested_InState_Restores_Previous_State_Scope() conn.Disconnect(); // should not throw (setup registered in outer scope) // In "inner" state, Connect should work (it was set up in inner scope) - mock.SetState("inner"); + Mock.SetState(mock, "inner"); conn.Connect(); // should not throw } @@ -199,18 +199,18 @@ public async Task SetState_Null_Clears_State() mock.GetStatus().Returns("NO_STATE"); // State-scoped setup — added second, wins when in "connected" state - mock.InState("connected", m => + Mock.InState(mock, "connected", m => { m.GetStatus().Returns("ONLINE"); }); - mock.SetState("connected"); + Mock.SetState(mock, "connected"); IConnection conn = mock.Object; await Assert.That(conn.GetStatus()).IsEqualTo("ONLINE"); // Clear state — scoped setup no longer matches, global setup wins - mock.SetState(null); + Mock.SetState(mock, null); await Assert.That(conn.GetStatus()).IsEqualTo("NO_STATE"); } @@ -218,9 +218,9 @@ public async Task SetState_Null_Clears_State() public async Task Reset_Clears_State() { var mock = Mock.Of(); - mock.SetState("connected"); + Mock.SetState(mock, "connected"); - mock.Reset(); + Mock.Reset(mock); // After reset, Engine.CurrentState should be null await Assert.That(mock.Engine.CurrentState).IsNull(); @@ -230,9 +230,9 @@ public async Task Reset_Clears_State() public async Task Verify_Works_With_State_Scoped_Setups() { var mock = Mock.Of(); - mock.SetState("disconnected"); + Mock.SetState(mock, "disconnected"); - mock.InState("disconnected", m => + Mock.InState(mock, "disconnected", m => { m.Connect().TransitionsTo("connected"); }); diff --git a/TUnit.Mocks.Tests/StrictModeTests.cs b/TUnit.Mocks.Tests/StrictModeTests.cs index 336674622a..e980c9461a 100644 --- a/TUnit.Mocks.Tests/StrictModeTests.cs +++ b/TUnit.Mocks.Tests/StrictModeTests.cs @@ -65,7 +65,7 @@ public async Task Strict_Configured_Void_Method_Does_Not_Throw() // Act & Assert — configured void method should not throw ICalculator calc = mock.Object; calc.Log("expected message"); - await Assert.That(mock.Behavior).IsEqualTo(MockBehavior.Strict); + await Assert.That(Mock.GetBehavior(mock)).IsEqualTo(MockBehavior.Strict); } [Test] @@ -197,6 +197,6 @@ public async Task Loose_Is_Default_Behavior() // Assert await Assert.That(result).IsEqualTo(0); - await Assert.That(mock.Behavior).IsEqualTo(MockBehavior.Loose); + await Assert.That(Mock.GetBehavior(mock)).IsEqualTo(MockBehavior.Loose); } } diff --git a/TUnit.Mocks.Tests/VerifyAllTests.cs b/TUnit.Mocks.Tests/VerifyAllTests.cs index 791f72dba3..6d437ee99b 100644 --- a/TUnit.Mocks.Tests/VerifyAllTests.cs +++ b/TUnit.Mocks.Tests/VerifyAllTests.cs @@ -21,7 +21,7 @@ public async Task VerifyAll_Passes_When_All_Setups_Invoked() svc.GetValue("key"); svc.Process(1); - mock.VerifyAll(); + Mock.VerifyAll(mock); await Assert.That(true).IsTrue(); } @@ -35,7 +35,7 @@ public async Task VerifyAll_Fails_When_Setup_Not_Invoked() var svc = mock.Object; svc.GetValue("key"); // Only call GetValue, not Process - var ex = Assert.Throws(() => mock.VerifyAll()); + var ex = Assert.Throws(() => Mock.VerifyAll(mock)); await Assert.That(ex.Message).Contains("Process"); } @@ -45,7 +45,7 @@ public async Task VerifyAll_Fails_When_No_Setups_Called() var mock = Mock.Of(); mock.GetValue(Arg.Any()).Returns("value"); - var ex = Assert.Throws(() => mock.VerifyAll()); + var ex = Assert.Throws(() => Mock.VerifyAll(mock)); await Assert.That(ex.Message).Contains("GetValue"); } @@ -53,7 +53,7 @@ public async Task VerifyAll_Fails_When_No_Setups_Called() public async Task VerifyAll_Passes_When_No_Setups_Registered() { var mock = Mock.Of(); - mock.VerifyAll(); // No setups = nothing to verify + Mock.VerifyAll(mock); // No setups = nothing to verify await Assert.That(true).IsTrue(); } @@ -65,7 +65,7 @@ public async Task VerifyAll_Multiple_Uninvoked_Shows_All() mock.Process(42); // Don't call anything - var ex = Assert.Throws(() => mock.VerifyAll()); + var ex = Assert.Throws(() => Mock.VerifyAll(mock)); await Assert.That(ex.Message).Contains("GetValue"); await Assert.That(ex.Message).Contains("Process"); } diff --git a/TUnit.Mocks.Tests/VerifyNoOtherCallsTests.cs b/TUnit.Mocks.Tests/VerifyNoOtherCallsTests.cs index e061933606..8ec10cb614 100644 --- a/TUnit.Mocks.Tests/VerifyNoOtherCallsTests.cs +++ b/TUnit.Mocks.Tests/VerifyNoOtherCallsTests.cs @@ -21,7 +21,7 @@ public async Task VerifyNoOtherCalls_Passes_When_All_Calls_Verified() svc.GetValue("key1"); mock.GetValue("key1").WasCalled(Times.Once); - mock.VerifyNoOtherCalls(); + Mock.VerifyNoOtherCalls(mock); await Assert.That(true).IsTrue(); } @@ -39,7 +39,7 @@ public async Task VerifyNoOtherCalls_Fails_When_Unverified_Calls_Exist() // Only verify GetValue, not Process mock.GetValue("key1").WasCalled(Times.Once); - var ex = Assert.Throws(() => mock.VerifyNoOtherCalls()); + var ex = Assert.Throws(() => Mock.VerifyNoOtherCalls(mock)); await Assert.That(ex.Message).Contains("Process(42)"); } @@ -47,7 +47,7 @@ public async Task VerifyNoOtherCalls_Fails_When_Unverified_Calls_Exist() public async Task VerifyNoOtherCalls_Passes_When_No_Calls_Made() { var mock = Mock.Of(); - mock.VerifyNoOtherCalls(); + Mock.VerifyNoOtherCalls(mock); await Assert.That(true).IsTrue(); } @@ -60,8 +60,8 @@ public async Task VerifyNoOtherCalls_Works_After_Reset() var svc = mock.Object; svc.GetValue("key1"); - mock.Reset(); - mock.VerifyNoOtherCalls(); // should pass — history cleared + Mock.Reset(mock); + Mock.VerifyNoOtherCalls(mock); // should pass — history cleared await Assert.That(true).IsTrue(); } @@ -78,7 +78,7 @@ public async Task VerifyNoOtherCalls_Multiple_Unverified_Shows_All() svc.Reset(); // Verify none - var ex = Assert.Throws(() => mock.VerifyNoOtherCalls()); + var ex = Assert.Throws(() => Mock.VerifyNoOtherCalls(mock)); await Assert.That(ex.Message).Contains("GetValue(a)"); await Assert.That(ex.Message).Contains("Process(1)"); } From fc85117b8ebcd523f1472f1d3db0ae1dca1af33b Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:20:39 +0000 Subject: [PATCH 05/10] docs(mocks): update examples to use static Mock helper API --- docs/docs/test-authoring/mocking/advanced.md | 22 +++++++++---------- docs/docs/test-authoring/mocking/setup.md | 2 +- .../test-authoring/mocking/verification.md | 6 ++--- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/docs/test-authoring/mocking/advanced.md b/docs/docs/test-authoring/mocking/advanced.md index e5b4a71f76..ffaa824c07 100644 --- a/docs/docs/test-authoring/mocking/advanced.md +++ b/docs/docs/test-authoring/mocking/advanced.md @@ -74,15 +74,15 @@ public interface IConnection } var mock = Mock.Of(); -mock.SetState("disconnected"); +Mock.SetState(mock, "disconnected"); -mock.InState("disconnected", m => +Mock.InState(mock, "disconnected", m => { m.GetStatus().Returns("OFFLINE"); m.Connect().TransitionsTo("connected"); }); -mock.InState("connected", m => +Mock.InState(mock, "connected", m => { m.GetStatus().Returns("ONLINE"); m.Disconnect().TransitionsTo("disconnected"); @@ -102,9 +102,9 @@ status = mock.Object.GetStatus(); // "OFFLINE" | Method | Description | |---|---| -| `mock.SetState("name")` | Set the current state | -| `mock.SetState(null)` | Clear state (all setups match) | -| `mock.InState("name", configure)` | Register setups scoped to a state | +| `Mock.SetState(mock, "name")` | Set the current state | +| `Mock.SetState(mock, null)` | Clear state (all setups match) | +| `Mock.InState(mock, "name", configure)` | Register setups scoped to a state | | `.TransitionsTo("name")` | Transition state after method call (on setup chain) | ## Recursive / Auto-Mocking @@ -129,7 +129,7 @@ var serviceB = mock.Object.GetServiceB(); // serviceB is not null — it's a working mock // Configure the auto-mock -var autoMock = mock.GetAutoMock("GetServiceB"); +var autoMock = Mock.GetAutoMock(mock, "GetServiceB"); autoMock.GetValue().Returns(42); var value = serviceB.GetValue(); // 42 @@ -187,7 +187,7 @@ mock.Delete(Arg.Any()); svc.GetUser(1); // Delete was never called -var diag = mock.GetDiagnostics(); +var diag = Mock.GetDiagnostics(mock); diag.TotalSetups; // 2 diag.ExercisedSetups; // 1 diag.UnusedSetups; // [Delete(Arg.Any())] @@ -215,7 +215,7 @@ public class TestDefaults : IDefaultValueProvider } var mock = Mock.Of(); -mock.DefaultValueProvider = new TestDefaults(); +Mock.SetDefaultValueProvider(mock, new TestDefaults()); var name = mock.Object.GetName(); // "test-default" (no setup needed) var count = mock.Object.GetCount(); // -1 @@ -231,10 +231,10 @@ Clear all setups, call history, state, and auto-tracked property values: mock.GetUser(Arg.Any()).Returns(new User("Alice")); svc.GetUser(1); -mock.Reset(); +Mock.Reset(mock); svc.GetUser(1); // returns default (setup cleared) -mock.Invocations.Count; // 0 (history cleared) +Mock.Invocations(mock).Count; // 0 (history cleared) ``` The `SetupAllProperties()` flag is preserved across resets. diff --git a/docs/docs/test-authoring/mocking/setup.md b/docs/docs/test-authoring/mocking/setup.md index 579651d467..034bb67e11 100644 --- a/docs/docs/test-authoring/mocking/setup.md +++ b/docs/docs/test-authoring/mocking/setup.md @@ -116,7 +116,7 @@ Call `SetupAllProperties()` to make properties behave like real auto-properties ```csharp var mock = Mock.Of(); -mock.SetupAllProperties(); +Mock.SetupAllProperties(mock); mock.Object.Name = "Alice"; var name = mock.Object.Name; // "Alice" diff --git a/docs/docs/test-authoring/mocking/verification.md b/docs/docs/test-authoring/mocking/verification.md index f49041368b..d2a93c9238 100644 --- a/docs/docs/test-authoring/mocking/verification.md +++ b/docs/docs/test-authoring/mocking/verification.md @@ -104,7 +104,7 @@ mock.Delete(Arg.Any()); svc.GetUser(1); svc.Delete(2); -mock.VerifyAll(); // passes — both setups were invoked +Mock.VerifyAll(mock); // passes — both setups were invoked ``` If any setup was never called, `VerifyAll` throws listing the uninvoked setups. @@ -120,7 +120,7 @@ svc.Delete(2); mock.GetUser(1).WasCalled(Times.Once); mock.Delete(2).WasCalled(Times.Once); -mock.VerifyNoOtherCalls(); // passes — all calls accounted for +Mock.VerifyNoOtherCalls(mock); // passes — all calls accounted for ``` If there are unverified calls, `VerifyNoOtherCalls` throws listing them. @@ -146,7 +146,7 @@ This integrates with TUnit's assertion engine — failures appear as assertion e Access the raw call history for custom inspection: ```csharp -var calls = mock.Invocations; +var calls = Mock.Invocations(mock); await Assert.That(calls).HasCount().EqualTo(3); await Assert.That(calls[0].MemberName).IsEqualTo("GetUser"); From 44f40243b5b08d033ca1fdb7ff7a3777f69b5f05 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:36:10 +0000 Subject: [PATCH 06/10] feat(mocks): replace GetAutoMock with Mock.Get for retrieving mock wrappers Add Mock.Get(T obj) to retrieve Mock from any mock object. Uses ConditionalWeakTable registry populated in Mock constructor. Removes GetAutoMock (magic string, redundant type params). Mock.Get works for auto-mocked return values, or any object from Mock.Of. --- TUnit.Mocks.Tests/AdditionalCoverageTests.cs | 22 +++---- TUnit.Mocks.Tests/AutoMockTests.cs | 8 +-- TUnit.Mocks/Mock.cs | 65 ++++++++++++++------ TUnit.Mocks/MockOfT.cs | 1 + docs/docs/test-authoring/mocking/advanced.md | 6 +- 5 files changed, 64 insertions(+), 38 deletions(-) diff --git a/TUnit.Mocks.Tests/AdditionalCoverageTests.cs b/TUnit.Mocks.Tests/AdditionalCoverageTests.cs index d0b9608cff..fbbdb14a82 100644 --- a/TUnit.Mocks.Tests/AdditionalCoverageTests.cs +++ b/TUnit.Mocks.Tests/AdditionalCoverageTests.cs @@ -178,23 +178,22 @@ public async Task IMock_ObjectInstance_Returns_Same_As_Object() } } -// ─── GetAutoMock Error Path ───────────────────────────────────────────────── +// ─── Mock.Get Error Path ──────────────────────────────────────────────────── -public class AutoMockErrorPathTests +public class MockGetErrorPathTests { [Test] - public async Task GetAutoMock_Throws_When_No_AutoMock_Exists() + public async Task Mock_Get_Throws_For_Non_Mock_Object() { - var mock = Mock.Of(); + // A plain object, not created by Mock.Of + var notAMock = new List(); - // No method has been called, so no auto-mock was created var ex = Assert.Throws(() => { - Mock.GetAutoMock(mock, "NonExistentMethod"); + Mock.Get(notAMock); }); - await Assert.That(ex.Message).Contains("No auto-mock found"); - await Assert.That(ex.Message).Contains("NonExistentMethod"); + await Assert.That(ex.Message).Contains("is not a mock"); } } @@ -283,10 +282,11 @@ public async Task Invocations_Sequence_Numbers_Are_Monotonically_Increasing() mock.Object.GetName(); mock.Object.Log("msg"); - for (int i = 1; i < Mock.Invocations(mock).Count; i++) + var invocations = Mock.Invocations(mock); + for (int i = 1; i < invocations.Count; i++) { - await Assert.That(Mock.Invocations(mock)[i].SequenceNumber) - .IsGreaterThan(Mock.Invocations(mock)[i - 1].SequenceNumber); + await Assert.That(invocations[i].SequenceNumber) + .IsGreaterThan(invocations[i - 1].SequenceNumber); } } } diff --git a/TUnit.Mocks.Tests/AutoMockTests.cs b/TUnit.Mocks.Tests/AutoMockTests.cs index 675d12fe09..c512cb7127 100644 --- a/TUnit.Mocks.Tests/AutoMockTests.cs +++ b/TUnit.Mocks.Tests/AutoMockTests.cs @@ -122,16 +122,14 @@ public async Task Nested_Chain_Returns_Default_Values() } [Test] - public async Task Auto_Mock_Configurable_Via_GetAutoMock() + public async Task Auto_Mock_Configurable_Via_Mock_Get() { // Arrange var mock = Mock.Of(); - // Trigger auto-mock creation + // Trigger auto-mock creation and retrieve the wrapper var serviceB = mock.Object.GetServiceB(); - - // Retrieve and configure the auto-mock - var autoMock = Mock.GetAutoMock(mock, "GetServiceB"); + var autoMock = Mock.Get(serviceB); autoMock.GetValue().Returns(42); // Act diff --git a/TUnit.Mocks/Mock.cs b/TUnit.Mocks/Mock.cs index a58d4f5da2..b19f09e4d0 100644 --- a/TUnit.Mocks/Mock.cs +++ b/TUnit.Mocks/Mock.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Runtime.CompilerServices; using TUnit.Mocks.Diagnostics; using TUnit.Mocks.Verification; @@ -9,6 +10,10 @@ namespace TUnit.Mocks; /// public static class Mock { + // Maps mock implementation objects back to their Mock wrappers. + // ConditionalWeakTable so mocks can be GC'd normally. + private static readonly ConditionalWeakTable _objectToMock = new(); + // The source generator registers factories via this method at module initialization time. // ConcurrentDictionary is used because module initializers from multiple assemblies // can run concurrently when test assemblies are loaded in parallel. @@ -26,6 +31,47 @@ public static class Mock // Separate registry for wrap mock factories that accept a real instance. private static readonly ConcurrentDictionary> _wrapFactories = new(); + /// + /// Registers the mapping from a mock implementation object to its wrapper. + /// Called from the constructor. Not intended for direct use. + /// + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + public static void Register(object mockObject, IMock mockWrapper) + { +#if NET7_0_OR_GREATER + _objectToMock.AddOrUpdate(mockObject, mockWrapper); +#else + // ConditionalWeakTable.AddOrUpdate not available before .NET 7. + // Each mock object is unique so Add should not throw, but be safe. + try { _objectToMock.Add(mockObject, mockWrapper); } + catch (ArgumentException) { _objectToMock.Remove(mockObject); _objectToMock.Add(mockObject, mockWrapper); } +#endif + } + + /// + /// Retrieves the wrapper for a mock implementation object. + /// Use this to access the mock wrapper from auto-mocked return values or any mocked object. + /// + /// + /// + /// var mock = Mock.Of<IServiceA>(); + /// var serviceB = mock.Object.GetServiceB(); // auto-mocked + /// var autoMock = Mock.Get(serviceB); // get the wrapper + /// autoMock.GetValue().Returns(42); + /// + /// + public static Mock Get(T mockedObject) where T : class + { + if (_objectToMock.TryGetValue(mockedObject, out var mock)) + { + return (Mock)mock; + } + + throw new InvalidOperationException( + $"The object of type '{typeof(T).Name}' is not a mock. " + + $"Mock.Get can only be used with objects created by Mock.Of, auto-mocking, or other Mock factory methods."); + } + /// /// Registers a factory for creating mocks of type T. Called by generated code. /// Not intended for direct use. @@ -304,25 +350,6 @@ public static void VerifyAll(Mock mock) where T : class public static void VerifyNoOtherCalls(Mock mock) where T : class => ((IMock)mock).VerifyNoOtherCalls(); - /// - /// Retrieves the auto-mock wrapper for a child interface returned by this mock. - /// Use this to configure auto-mocked return values. - /// - public static Mock GetAutoMock(Mock mock, string memberName) - where T : class - where TChild : class - { - var cacheKey = memberName + "|" + typeof(TChild).FullName; - if (mock.Engine.TryGetAutoMock(cacheKey, out var autoMock)) - { - return (Mock)autoMock; - } - - throw new InvalidOperationException( - $"No auto-mock found for member '{memberName}' returning type '{typeof(TChild).Name}'. " + - $"Ensure the method was called at least once before retrieving its auto-mock."); - } - /// /// Returns a diagnostic report of this mock's setup coverage and call matching. /// diff --git a/TUnit.Mocks/MockOfT.cs b/TUnit.Mocks/MockOfT.cs index 61c3f05f02..8c5195e55d 100644 --- a/TUnit.Mocks/MockOfT.cs +++ b/TUnit.Mocks/MockOfT.cs @@ -26,6 +26,7 @@ public Mock(T mockObject, MockEngine engine) { Engine = engine; Object = mockObject; + Mock.Register(mockObject, this); } /// diff --git a/docs/docs/test-authoring/mocking/advanced.md b/docs/docs/test-authoring/mocking/advanced.md index ffaa824c07..d2c82c76ff 100644 --- a/docs/docs/test-authoring/mocking/advanced.md +++ b/docs/docs/test-authoring/mocking/advanced.md @@ -128,14 +128,14 @@ var mock = Mock.Of(); var serviceB = mock.Object.GetServiceB(); // serviceB is not null — it's a working mock -// Configure the auto-mock -var autoMock = Mock.GetAutoMock(mock, "GetServiceB"); +// Configure the auto-mock via Mock.Get +var autoMock = Mock.Get(serviceB); autoMock.GetValue().Returns(42); var value = serviceB.GetValue(); // 42 ``` -Auto-mocks are cached — calling the same method returns the same mock instance. +Use `Mock.Get(obj)` to retrieve the `Mock` wrapper for any mock object — auto-mocked return values, or any object created by `Mock.Of`. Auto-mocks are cached — calling the same method returns the same mock instance. ## MockRepository From 8f1d84bb053fc935685800ec18292beafc687fd5 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:37:57 +0000 Subject: [PATCH 07/10] refactor(mocks): rename Invocations to GetInvocations for consistency All static accessors now use Get prefix: GetBehavior, GetDiagnostics, GetDefaultValueProvider, GetInvocations. Makes it clear these are method calls with potential cost (GetInvocations allocates an array). --- TUnit.Mocks.Tests/AdditionalCoverageTests.cs | 12 ++++++------ TUnit.Mocks.Tests/InvocationsTests.cs | 12 ++++++------ TUnit.Mocks.Tests/MockRepositoryTests.cs | 4 ++-- TUnit.Mocks.Tests/MultipleInterfaceTests.cs | 4 ++-- TUnit.Mocks.Tests/ProtectedMemberTests.cs | 2 +- TUnit.Mocks/Mock.cs | 2 +- docs/docs/test-authoring/mocking/advanced.md | 2 +- docs/docs/test-authoring/mocking/verification.md | 2 +- 8 files changed, 20 insertions(+), 20 deletions(-) diff --git a/TUnit.Mocks.Tests/AdditionalCoverageTests.cs b/TUnit.Mocks.Tests/AdditionalCoverageTests.cs index fbbdb14a82..d06ea80804 100644 --- a/TUnit.Mocks.Tests/AdditionalCoverageTests.cs +++ b/TUnit.Mocks.Tests/AdditionalCoverageTests.cs @@ -266,11 +266,11 @@ public async Task Invocations_Are_In_Call_Order() mock.Object.GetName(); mock.Object.Add(3, 4); - await Assert.That(Mock.Invocations(mock)).HasCount().EqualTo(4); - await Assert.That(Mock.Invocations(mock)[0].MemberName).IsEqualTo("Add"); - await Assert.That(Mock.Invocations(mock)[1].MemberName).IsEqualTo("Log"); - await Assert.That(Mock.Invocations(mock)[2].MemberName).IsEqualTo("GetName"); - await Assert.That(Mock.Invocations(mock)[3].MemberName).IsEqualTo("Add"); + await Assert.That(Mock.GetInvocations(mock)).HasCount().EqualTo(4); + await Assert.That(Mock.GetInvocations(mock)[0].MemberName).IsEqualTo("Add"); + await Assert.That(Mock.GetInvocations(mock)[1].MemberName).IsEqualTo("Log"); + await Assert.That(Mock.GetInvocations(mock)[2].MemberName).IsEqualTo("GetName"); + await Assert.That(Mock.GetInvocations(mock)[3].MemberName).IsEqualTo("Add"); } [Test] @@ -282,7 +282,7 @@ public async Task Invocations_Sequence_Numbers_Are_Monotonically_Increasing() mock.Object.GetName(); mock.Object.Log("msg"); - var invocations = Mock.Invocations(mock); + var invocations = Mock.GetInvocations(mock); for (int i = 1; i < invocations.Count; i++) { await Assert.That(invocations[i].SequenceNumber) diff --git a/TUnit.Mocks.Tests/InvocationsTests.cs b/TUnit.Mocks.Tests/InvocationsTests.cs index 155cec0b79..f44831b6d6 100644 --- a/TUnit.Mocks.Tests/InvocationsTests.cs +++ b/TUnit.Mocks.Tests/InvocationsTests.cs @@ -19,7 +19,7 @@ public async Task Invocations_Returns_All_Calls() svc.GetValue("key2"); svc.Process(42); - await Assert.That(Mock.Invocations(mock).Count).IsEqualTo(3); + await Assert.That(Mock.GetInvocations(mock).Count).IsEqualTo(3); } [Test] @@ -32,8 +32,8 @@ public async Task Invocations_Contains_Correct_Method_Names() svc.GetValue("key1"); svc.Process(99); - await Assert.That(Mock.Invocations(mock)[0].MemberName).IsEqualTo("GetValue"); - await Assert.That(Mock.Invocations(mock)[1].MemberName).IsEqualTo("Process"); + await Assert.That(Mock.GetInvocations(mock)[0].MemberName).IsEqualTo("GetValue"); + await Assert.That(Mock.GetInvocations(mock)[1].MemberName).IsEqualTo("Process"); } [Test] @@ -45,14 +45,14 @@ public async Task Invocations_Contains_Correct_Arguments() var svc = mock.Object; svc.GetValue("hello"); - await Assert.That(Mock.Invocations(mock)[0].Arguments[0]).IsEqualTo("hello"); + await Assert.That(Mock.GetInvocations(mock)[0].Arguments[0]).IsEqualTo("hello"); } [Test] public async Task Invocations_Is_Empty_When_No_Calls_Made() { var mock = Mock.Of(); - await Assert.That(Mock.Invocations(mock).Count).IsEqualTo(0); + await Assert.That(Mock.GetInvocations(mock).Count).IsEqualTo(0); } [Test] @@ -66,6 +66,6 @@ public async Task Invocations_Is_Empty_After_Reset() Mock.Reset(mock); - await Assert.That(Mock.Invocations(mock).Count).IsEqualTo(0); + await Assert.That(Mock.GetInvocations(mock).Count).IsEqualTo(0); } } diff --git a/TUnit.Mocks.Tests/MockRepositoryTests.cs b/TUnit.Mocks.Tests/MockRepositoryTests.cs index 92d4f3c51d..fc2025cf65 100644 --- a/TUnit.Mocks.Tests/MockRepositoryTests.cs +++ b/TUnit.Mocks.Tests/MockRepositoryTests.cs @@ -137,8 +137,8 @@ public async Task Repository_Reset_Clears_All_Mocks() // Assert — setups and history are cleared await Assert.That(serviceMock.Object.GetData(1)).IsEmpty(); // no setup, returns smart default - await Assert.That(Mock.Invocations(serviceMock)).Count().IsEqualTo(1); // only the new call - await Assert.That(Mock.Invocations(loggerMock)).Count().IsEqualTo(0); // history cleared + await Assert.That(Mock.GetInvocations(serviceMock)).Count().IsEqualTo(1); // only the new call + await Assert.That(Mock.GetInvocations(loggerMock)).Count().IsEqualTo(0); // history cleared } [Test] diff --git a/TUnit.Mocks.Tests/MultipleInterfaceTests.cs b/TUnit.Mocks.Tests/MultipleInterfaceTests.cs index d994446bc8..3cadd68826 100644 --- a/TUnit.Mocks.Tests/MultipleInterfaceTests.cs +++ b/TUnit.Mocks.Tests/MultipleInterfaceTests.cs @@ -114,7 +114,7 @@ public async Task Multi_Mock_Shares_Single_Engine() ((IMultiDisposable)mock.Object).Dispose(); // Assert — all calls recorded in invocations - await Assert.That(Mock.Invocations(mock)).Count().IsEqualTo(2); + await Assert.That(Mock.GetInvocations(mock)).Count().IsEqualTo(2); } [Test] @@ -180,7 +180,7 @@ public async Task Mock_Of_Four_Interfaces_Tracks_All_Calls() ((IMultiCloneable)mock.Object).Clone(); // Assert — all 4 calls tracked - await Assert.That(Mock.Invocations(mock)).Count().IsEqualTo(4); + await Assert.That(Mock.GetInvocations(mock)).Count().IsEqualTo(4); } [Test] diff --git a/TUnit.Mocks.Tests/ProtectedMemberTests.cs b/TUnit.Mocks.Tests/ProtectedMemberTests.cs index 3ecbfd89e7..52f6120882 100644 --- a/TUnit.Mocks.Tests/ProtectedMemberTests.cs +++ b/TUnit.Mocks.Tests/ProtectedMemberTests.cs @@ -88,7 +88,7 @@ public async Task Protected_Method_Calls_Are_Recorded_In_Invocations() mock.Object.ProcessAndFormat(7); // Assert — both ComputeValue (base call) and FormatResult are recorded - await Assert.That(Mock.Invocations(mock)).Count().IsGreaterThanOrEqualTo(2); + await Assert.That(Mock.GetInvocations(mock)).Count().IsGreaterThanOrEqualTo(2); } [Test] diff --git a/TUnit.Mocks/Mock.cs b/TUnit.Mocks/Mock.cs index b19f09e4d0..efd99aa707 100644 --- a/TUnit.Mocks/Mock.cs +++ b/TUnit.Mocks/Mock.cs @@ -304,7 +304,7 @@ public static void VerifyInOrder(Action verificationActions) // ────────────────────────────────────────────────────────── /// All calls made to this mock, in order. - public static IReadOnlyList Invocations(Mock mock) where T : class + public static IReadOnlyList GetInvocations(Mock mock) where T : class => mock.Engine.GetAllCalls(); /// Returns the mock behavior (Loose or Strict). diff --git a/docs/docs/test-authoring/mocking/advanced.md b/docs/docs/test-authoring/mocking/advanced.md index d2c82c76ff..61b07785e7 100644 --- a/docs/docs/test-authoring/mocking/advanced.md +++ b/docs/docs/test-authoring/mocking/advanced.md @@ -234,7 +234,7 @@ svc.GetUser(1); Mock.Reset(mock); svc.GetUser(1); // returns default (setup cleared) -Mock.Invocations(mock).Count; // 0 (history cleared) +Mock.GetInvocations(mock).Count; // 0 (history cleared) ``` The `SetupAllProperties()` flag is preserved across resets. diff --git a/docs/docs/test-authoring/mocking/verification.md b/docs/docs/test-authoring/mocking/verification.md index d2a93c9238..de3ff19de9 100644 --- a/docs/docs/test-authoring/mocking/verification.md +++ b/docs/docs/test-authoring/mocking/verification.md @@ -146,7 +146,7 @@ This integrates with TUnit's assertion engine — failures appear as assertion e Access the raw call history for custom inspection: ```csharp -var calls = Mock.Invocations(mock); +var calls = Mock.GetInvocations(mock); await Assert.That(calls).HasCount().EqualTo(3); await Assert.That(calls[0].MemberName).IsEqualTo("GetUser"); From 6f02aaee6368486df049f7922cfb31733a38ac6e Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:46:23 +0000 Subject: [PATCH 08/10] refactor(mocks): remove Engine from Mock public surface Introduce IMockEngineAccess with explicit implementation so Engine is no longer visible on Mock. Add Mock.GetEngine static helper. Update source generator to emit Mock.GetEngine(mock) in generated code. MockMemberNames now only reserves Object + System.Object members. --- ...nheriting_Multiple_Interfaces.verified.txt | 6 ++-- .../Interface_With_Async_Methods.verified.txt | 8 ++--- .../Interface_With_Events.verified.txt | 6 ++-- ...nterface_With_Generic_Methods.verified.txt | 6 ++-- .../Interface_With_Mixed_Members.verified.txt | 12 +++---- ...rface_With_Out_Ref_Parameters.verified.txt | 6 ++-- ...rface_With_Overloaded_Methods.verified.txt | 8 ++--- .../Interface_With_Properties.verified.txt | 6 ++-- ...ace_With_RefStruct_Parameters.verified.txt | 10 +++--- .../Multi_Method_Interface.verified.txt | 6 ++-- ...ple_Interface_With_One_Method.verified.txt | 2 +- .../Builders/MockEventsBuilder.cs | 2 +- .../Builders/MockMembersBuilder.cs | 12 +++---- TUnit.Mocks.Tests/StateMachineTests.cs | 4 +-- TUnit.Mocks/IMockEngineAccessOfT.cs | 13 ++++++++ TUnit.Mocks/Mock.cs | 32 ++++++++++++------- TUnit.Mocks/MockOfT.cs | 17 +++++----- 17 files changed, 89 insertions(+), 67 deletions(-) create mode 100644 TUnit.Mocks/IMockEngineAccessOfT.cs diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_Inheriting_Multiple_Interfaces.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_Inheriting_Multiple_Interfaces.verified.txt index 519639f46b..a9052e800a 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_Inheriting_Multiple_Interfaces.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_Inheriting_Multiple_Interfaces.verified.txt @@ -75,19 +75,19 @@ namespace TUnit.Mocks.Generated public static global::TUnit.Mocks.VoidMockMethodCall Flush(this global::TUnit.Mocks.Mock mock) { var matchers = global::System.Array.Empty(); - return new global::TUnit.Mocks.VoidMockMethodCall(mock.Engine, 0, "Flush", matchers); + return new global::TUnit.Mocks.VoidMockMethodCall(global::TUnit.Mocks.Mock.GetEngine(mock), 0, "Flush", matchers); } public static global::TUnit.Mocks.MockMethodCall Read(this global::TUnit.Mocks.Mock mock) { var matchers = global::System.Array.Empty(); - return new global::TUnit.Mocks.MockMethodCall(mock.Engine, 1, "Read", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.Mock.GetEngine(mock), 1, "Read", matchers); } public static IReadWriter_Write_M2_MockCall Write(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg data) { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { data.Matcher }; - return new IReadWriter_Write_M2_MockCall(mock.Engine, 2, "Write", matchers); + return new IReadWriter_Write_M2_MockCall(global::TUnit.Mocks.Mock.GetEngine(mock), 2, "Write", matchers); } } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Async_Methods.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Async_Methods.verified.txt index 1518968e91..4f0d0c19f3 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Async_Methods.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Async_Methods.verified.txt @@ -112,25 +112,25 @@ namespace TUnit.Mocks.Generated public static IAsyncService_GetValueAsync_M0_MockCall GetValueAsync(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg key) { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { key.Matcher }; - return new IAsyncService_GetValueAsync_M0_MockCall(mock.Engine, 0, "GetValueAsync", matchers); + return new IAsyncService_GetValueAsync_M0_MockCall(global::TUnit.Mocks.Mock.GetEngine(mock), 0, "GetValueAsync", matchers); } public static global::TUnit.Mocks.VoidMockMethodCall DoWorkAsync(this global::TUnit.Mocks.Mock mock) { var matchers = global::System.Array.Empty(); - return new global::TUnit.Mocks.VoidMockMethodCall(mock.Engine, 1, "DoWorkAsync", matchers); + return new global::TUnit.Mocks.VoidMockMethodCall(global::TUnit.Mocks.Mock.GetEngine(mock), 1, "DoWorkAsync", matchers); } public static IAsyncService_ComputeAsync_M2_MockCall ComputeAsync(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg input) { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { input.Matcher }; - return new IAsyncService_ComputeAsync_M2_MockCall(mock.Engine, 2, "ComputeAsync", matchers); + return new IAsyncService_ComputeAsync_M2_MockCall(global::TUnit.Mocks.Mock.GetEngine(mock), 2, "ComputeAsync", matchers); } public static IAsyncService_InitializeAsync_M3_MockCall InitializeAsync(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg ct) { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { ct.Matcher }; - return new IAsyncService_InitializeAsync_M3_MockCall(mock.Engine, 3, "InitializeAsync", matchers); + return new IAsyncService_InitializeAsync_M3_MockCall(global::TUnit.Mocks.Mock.GetEngine(mock), 3, "InitializeAsync", matchers); } } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Events.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Events.verified.txt index be44e45cc5..4f8bac42ce 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Events.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Events.verified.txt @@ -15,7 +15,7 @@ namespace TUnit.Mocks.Generated { extension(global::TUnit.Mocks.Mock mock) { - public INotifier_MockEvents Events => new(mock.Engine); + public INotifier_MockEvents Events => new(global::TUnit.Mocks.Mock.GetEngine(mock)); } extension(INotifier_MockEvents events) @@ -117,12 +117,12 @@ namespace TUnit.Mocks.Generated public static INotifier_Notify_M0_MockCall Notify(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg message) { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { message.Matcher }; - return new INotifier_Notify_M0_MockCall(mock.Engine, 0, "Notify", matchers); + return new INotifier_Notify_M0_MockCall(global::TUnit.Mocks.Mock.GetEngine(mock), 0, "Notify", matchers); } public static void RaiseItemAdded(this global::TUnit.Mocks.Mock mock, string e) { - ((global::TUnit.Mocks.IRaisable)mock.Engine.Raisable!).RaiseEvent("ItemAdded", (object?)e); + ((global::TUnit.Mocks.IRaisable)global::TUnit.Mocks.Mock.GetEngine(mock).Raisable!).RaiseEvent("ItemAdded", (object?)e); } } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Generic_Methods.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Generic_Methods.verified.txt index dbf5e5a4d3..200e8989ca 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Generic_Methods.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Generic_Methods.verified.txt @@ -75,19 +75,19 @@ namespace TUnit.Mocks.Generated public static global::TUnit.Mocks.MockMethodCall GetById(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg id) where T : class { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { id.Matcher }; - return new global::TUnit.Mocks.MockMethodCall(mock.Engine, 0, "GetById", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.Mock.GetEngine(mock), 0, "GetById", matchers); } public static global::TUnit.Mocks.VoidMockMethodCall Save(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg entity) where T : class, new() { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { entity.Matcher }; - return new global::TUnit.Mocks.VoidMockMethodCall(mock.Engine, 1, "Save", matchers); + return new global::TUnit.Mocks.VoidMockMethodCall(global::TUnit.Mocks.Mock.GetEngine(mock), 1, "Save", matchers); } public static global::TUnit.Mocks.MockMethodCall Transform(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg input) where TInput : notnull where TResult : struct { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { input.Matcher }; - return new global::TUnit.Mocks.MockMethodCall(mock.Engine, 2, "Transform", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.Mock.GetEngine(mock), 2, "Transform", matchers); } } } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Mixed_Members.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Mixed_Members.verified.txt index 67f4b50f62..c8ea45824d 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Mixed_Members.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Mixed_Members.verified.txt @@ -15,7 +15,7 @@ namespace TUnit.Mocks.Generated { extension(global::TUnit.Mocks.Mock mock) { - public IService_MockEvents Events => new(mock.Engine); + public IService_MockEvents Events => new(global::TUnit.Mocks.Mock.GetEngine(mock)); } extension(IService_MockEvents events) @@ -141,27 +141,27 @@ namespace TUnit.Mocks.Generated public static IService_GetAsync_M3_MockCall GetAsync(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg id) { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { id.Matcher }; - return new IService_GetAsync_M3_MockCall(mock.Engine, 3, "GetAsync", matchers); + return new IService_GetAsync_M3_MockCall(global::TUnit.Mocks.Mock.GetEngine(mock), 3, "GetAsync", matchers); } public static IService_Process_M4_MockCall Process(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg data) { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { data.Matcher }; - return new IService_Process_M4_MockCall(mock.Engine, 4, "Process", matchers); + return new IService_Process_M4_MockCall(global::TUnit.Mocks.Mock.GetEngine(mock), 4, "Process", matchers); } extension(global::TUnit.Mocks.Mock mock) { public global::TUnit.Mocks.PropertyMockCall Name - => new(mock.Engine, 0, 1, "Name", true, true); + => new(global::TUnit.Mocks.Mock.GetEngine(mock), 0, 1, "Name", true, true); public global::TUnit.Mocks.PropertyMockCall Count - => new(mock.Engine, 2, 0, "Count", true, false); + => new(global::TUnit.Mocks.Mock.GetEngine(mock), 2, 0, "Count", true, false); } public static void RaiseStatusChanged(this global::TUnit.Mocks.Mock mock, string e) { - ((global::TUnit.Mocks.IRaisable)mock.Engine.Raisable!).RaiseEvent("StatusChanged", (object?)e); + ((global::TUnit.Mocks.IRaisable)global::TUnit.Mocks.Mock.GetEngine(mock).Raisable!).RaiseEvent("StatusChanged", (object?)e); } } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Out_Ref_Parameters.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Out_Ref_Parameters.verified.txt index 1c80691d99..54ea3591ec 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Out_Ref_Parameters.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Out_Ref_Parameters.verified.txt @@ -1,4 +1,4 @@ -// +// #nullable enable namespace TUnit.Mocks.Generated @@ -83,13 +83,13 @@ namespace TUnit.Mocks.Generated public static IDictionary_TryGetValue_M0_MockCall TryGetValue(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg key) { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { key.Matcher }; - return new IDictionary_TryGetValue_M0_MockCall(mock.Engine, 0, "TryGetValue", matchers); + return new IDictionary_TryGetValue_M0_MockCall(global::TUnit.Mocks.Mock.GetEngine(mock), 0, "TryGetValue", matchers); } public static IDictionary_Swap_M1_MockCall Swap(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg a, global::TUnit.Mocks.Arguments.Arg b) { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { a.Matcher, b.Matcher }; - return new IDictionary_Swap_M1_MockCall(mock.Engine, 1, "Swap", matchers); + return new IDictionary_Swap_M1_MockCall(global::TUnit.Mocks.Mock.GetEngine(mock), 1, "Swap", matchers); } } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Overloaded_Methods.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Overloaded_Methods.verified.txt index 57c183c550..3ff1a23f23 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Overloaded_Methods.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Overloaded_Methods.verified.txt @@ -80,25 +80,25 @@ namespace TUnit.Mocks.Generated public static IFormatter_Format_M0_MockCall Format(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg value) { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { value.Matcher }; - return new IFormatter_Format_M0_MockCall(mock.Engine, 0, "Format", matchers); + return new IFormatter_Format_M0_MockCall(global::TUnit.Mocks.Mock.GetEngine(mock), 0, "Format", matchers); } public static IFormatter_Format_M1_MockCall Format(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg value) { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { value.Matcher }; - return new IFormatter_Format_M1_MockCall(mock.Engine, 1, "Format", matchers); + return new IFormatter_Format_M1_MockCall(global::TUnit.Mocks.Mock.GetEngine(mock), 1, "Format", matchers); } public static IFormatter_Format_M2_MockCall Format(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg template, global::TUnit.Mocks.Arguments.Arg arg1) { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { template.Matcher, arg1.Matcher }; - return new IFormatter_Format_M2_MockCall(mock.Engine, 2, "Format", matchers); + return new IFormatter_Format_M2_MockCall(global::TUnit.Mocks.Mock.GetEngine(mock), 2, "Format", matchers); } public static IFormatter_Format_M3_MockCall Format(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg template, global::TUnit.Mocks.Arguments.Arg arg1, global::TUnit.Mocks.Arguments.Arg arg2) { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { template.Matcher, arg1.Matcher, arg2.Matcher }; - return new IFormatter_Format_M3_MockCall(mock.Engine, 3, "Format", matchers); + return new IFormatter_Format_M3_MockCall(global::TUnit.Mocks.Mock.GetEngine(mock), 3, "Format", matchers); } } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Properties.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Properties.verified.txt index a40e899791..fdd42f5115 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Properties.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Properties.verified.txt @@ -76,13 +76,13 @@ namespace TUnit.Mocks.Generated extension(global::TUnit.Mocks.Mock mock) { public global::TUnit.Mocks.PropertyMockCall Name - => new(mock.Engine, 0, 1, "Name", true, true); + => new(global::TUnit.Mocks.Mock.GetEngine(mock), 0, 1, "Name", true, true); public global::TUnit.Mocks.PropertyMockCall Count - => new(mock.Engine, 2, 0, "Count", true, false); + => new(global::TUnit.Mocks.Mock.GetEngine(mock), 2, 0, "Count", true, false); public global::TUnit.Mocks.PropertyMockCall IsOpen - => new(mock.Engine, 0, 4, "IsOpen", false, true); + => new(global::TUnit.Mocks.Mock.GetEngine(mock), 0, 4, "IsOpen", false, true); } } } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_RefStruct_Parameters.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_RefStruct_Parameters.verified.txt index 7e9308795d..85bbb1d4db 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_RefStruct_Parameters.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_RefStruct_Parameters.verified.txt @@ -86,13 +86,13 @@ namespace TUnit.Mocks.Generated public static global::TUnit.Mocks.VoidMockMethodCall Process(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.RefStructArg> data) { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { data.Matcher }; - return new global::TUnit.Mocks.VoidMockMethodCall(mock.Engine, 0, "Process", matchers); + return new global::TUnit.Mocks.VoidMockMethodCall(global::TUnit.Mocks.Mock.GetEngine(mock), 0, "Process", matchers); } #else public static global::TUnit.Mocks.VoidMockMethodCall Process(this global::TUnit.Mocks.Mock mock) { var matchers = global::System.Array.Empty(); - return new global::TUnit.Mocks.VoidMockMethodCall(mock.Engine, 0, "Process", matchers); + return new global::TUnit.Mocks.VoidMockMethodCall(global::TUnit.Mocks.Mock.GetEngine(mock), 0, "Process", matchers); } #endif @@ -100,20 +100,20 @@ namespace TUnit.Mocks.Generated public static global::TUnit.Mocks.MockMethodCall Parse(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.RefStructArg> text) { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { text.Matcher }; - return new global::TUnit.Mocks.MockMethodCall(mock.Engine, 1, "Parse", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.Mock.GetEngine(mock), 1, "Parse", matchers); } #else public static global::TUnit.Mocks.MockMethodCall Parse(this global::TUnit.Mocks.Mock mock) { var matchers = global::System.Array.Empty(); - return new global::TUnit.Mocks.MockMethodCall(mock.Engine, 1, "Parse", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.Mock.GetEngine(mock), 1, "Parse", matchers); } #endif public static global::TUnit.Mocks.MockMethodCall GetName(this global::TUnit.Mocks.Mock mock) { var matchers = global::System.Array.Empty(); - return new global::TUnit.Mocks.MockMethodCall(mock.Engine, 2, "GetName", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.Mock.GetEngine(mock), 2, "GetName", matchers); } } } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Method_Interface.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Method_Interface.verified.txt index 12489b560b..27477809c6 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Method_Interface.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Multi_Method_Interface.verified.txt @@ -75,19 +75,19 @@ namespace TUnit.Mocks.Generated public static ICalculator_Add_M0_MockCall Add(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg a, global::TUnit.Mocks.Arguments.Arg b) { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { a.Matcher, b.Matcher }; - return new ICalculator_Add_M0_MockCall(mock.Engine, 0, "Add", matchers); + return new ICalculator_Add_M0_MockCall(global::TUnit.Mocks.Mock.GetEngine(mock), 0, "Add", matchers); } public static ICalculator_Subtract_M1_MockCall Subtract(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg a, global::TUnit.Mocks.Arguments.Arg b) { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { a.Matcher, b.Matcher }; - return new ICalculator_Subtract_M1_MockCall(mock.Engine, 1, "Subtract", matchers); + return new ICalculator_Subtract_M1_MockCall(global::TUnit.Mocks.Mock.GetEngine(mock), 1, "Subtract", matchers); } public static global::TUnit.Mocks.VoidMockMethodCall Reset(this global::TUnit.Mocks.Mock mock) { var matchers = global::System.Array.Empty(); - return new global::TUnit.Mocks.VoidMockMethodCall(mock.Engine, 2, "Reset", matchers); + return new global::TUnit.Mocks.VoidMockMethodCall(global::TUnit.Mocks.Mock.GetEngine(mock), 2, "Reset", matchers); } } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Simple_Interface_With_One_Method.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Simple_Interface_With_One_Method.verified.txt index cfb21aec13..b054d42815 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Simple_Interface_With_One_Method.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Simple_Interface_With_One_Method.verified.txt @@ -65,7 +65,7 @@ namespace TUnit.Mocks.Generated public static IGreeter_Greet_M0_MockCall Greet(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg name) { var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { name.Matcher }; - return new IGreeter_Greet_M0_MockCall(mock.Engine, 0, "Greet", matchers); + return new IGreeter_Greet_M0_MockCall(global::TUnit.Mocks.Mock.GetEngine(mock), 0, "Greet", matchers); } } diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockEventsBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockEventsBuilder.cs index 2a5c11b06a..6ce8250eb3 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockEventsBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockEventsBuilder.cs @@ -31,7 +31,7 @@ public static string Build(MockTypeModel model) // Extension property on Mock — non-nullable, only present when type has events using (writer.Block($"extension(global::TUnit.Mocks.Mock<{model.FullyQualifiedName}> mock)")) { - writer.AppendLine($"public {safeName}_MockEvents Events => new(mock.Engine);"); + writer.AppendLine($"public {safeName}_MockEvents Events => new(global::TUnit.Mocks.Mock.GetEngine(mock));"); } writer.AppendLine(); diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs index 4dd6587d8d..d2a613bf6b 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs @@ -16,7 +16,7 @@ internal static class MockMembersBuilder private static readonly HashSet MockMemberNames = new(System.StringComparer.Ordinal) { - "Object", "Engine", + "Object", "GetHashCode", "GetType", "ToString", "Equals" }; @@ -555,15 +555,15 @@ private static void EmitMemberMethodBody(CodeWriter writer, MockMemberModel meth if (useTypedWrapper) { var wrapperName = GetWrapperName(safeName, method); - writer.AppendLine($"return new {wrapperName}(mock.Engine, {method.MemberId}, \"{method.Name}\", matchers);"); + writer.AppendLine($"return new {wrapperName}(global::TUnit.Mocks.Mock.GetEngine(mock), {method.MemberId}, \"{method.Name}\", matchers);"); } else if (method.IsVoid || method.IsRefStructReturn) { - writer.AppendLine($"return new global::TUnit.Mocks.VoidMockMethodCall(mock.Engine, {method.MemberId}, \"{method.Name}\", matchers);"); + writer.AppendLine($"return new global::TUnit.Mocks.VoidMockMethodCall(global::TUnit.Mocks.Mock.GetEngine(mock), {method.MemberId}, \"{method.Name}\", matchers);"); } else { - writer.AppendLine($"return new global::TUnit.Mocks.MockMethodCall<{setupReturnType}>(mock.Engine, {method.MemberId}, \"{method.Name}\", matchers);"); + writer.AppendLine($"return new global::TUnit.Mocks.MockMethodCall<{setupReturnType}>(global::TUnit.Mocks.Mock.GetEngine(mock), {method.MemberId}, \"{method.Name}\", matchers);"); } } } @@ -586,7 +586,7 @@ private static void GeneratePropertyExtensionBlock(CodeWriter writer, List {safePropName}"); - writer.AppendLine($" => new(mock.Engine, {getterMemberId}, {setterMemberId}, \"{prop.Name}\", {hasGetter}, {hasSetter});"); + writer.AppendLine($" => new(global::TUnit.Mocks.Mock.GetEngine(mock), {getterMemberId}, {setterMemberId}, \"{prop.Name}\", {hasGetter}, {hasSetter});"); } } } @@ -625,7 +625,7 @@ private static void GenerateRaiseExtensionMethods(CodeWriter writer, MockTypeMod using (writer.Block($"public static void Raise{evt.Name}({raiseParams})")) { - writer.AppendLine($"((global::TUnit.Mocks.IRaisable)mock.Engine.Raisable!).RaiseEvent(\"{evt.Name}\", {argsExpr});"); + writer.AppendLine($"((global::TUnit.Mocks.IRaisable)global::TUnit.Mocks.Mock.GetEngine(mock).Raisable!).RaiseEvent(\"{evt.Name}\", {argsExpr});"); } } } diff --git a/TUnit.Mocks.Tests/StateMachineTests.cs b/TUnit.Mocks.Tests/StateMachineTests.cs index 6c5ed2e8c9..28efdf386f 100644 --- a/TUnit.Mocks.Tests/StateMachineTests.cs +++ b/TUnit.Mocks.Tests/StateMachineTests.cs @@ -222,8 +222,8 @@ public async Task Reset_Clears_State() Mock.Reset(mock); - // After reset, Engine.CurrentState should be null - await Assert.That(mock.Engine.CurrentState).IsNull(); + // After reset, current state should be null + await Assert.That(Mock.GetEngine(mock).CurrentState).IsNull(); } [Test] diff --git a/TUnit.Mocks/IMockEngineAccessOfT.cs b/TUnit.Mocks/IMockEngineAccessOfT.cs new file mode 100644 index 0000000000..20acf053f4 --- /dev/null +++ b/TUnit.Mocks/IMockEngineAccessOfT.cs @@ -0,0 +1,13 @@ +using System.ComponentModel; + +namespace TUnit.Mocks; + +/// +/// Provides access to the mock engine for generated code. Not intended for direct use. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public interface IMockEngineAccess where T : class +{ + /// The mock engine instance. + MockEngine Engine { get; } +} diff --git a/TUnit.Mocks/Mock.cs b/TUnit.Mocks/Mock.cs index efd99aa707..856f5a1b62 100644 --- a/TUnit.Mocks/Mock.cs +++ b/TUnit.Mocks/Mock.cs @@ -129,7 +129,7 @@ public static void RegisterWrapFactory(Func> factory public static Mock Of(MockBehavior behavior, IDefaultValueProvider defaultValueProvider) where T : class { var mock = Of(behavior); - mock.Engine.DefaultValueProvider = defaultValueProvider; + GetEngine(mock).DefaultValueProvider = defaultValueProvider; return mock; } @@ -303,38 +303,45 @@ public static void VerifyInOrder(Action verificationActions) // Static helpers – expose control surface without cluttering Mock // ────────────────────────────────────────────────────────── + /// + /// Gets the mock engine for generated code. Not intended for direct use. + /// + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + public static MockEngine GetEngine(Mock mock) where T : class + => ((IMockEngineAccess)mock).Engine; + /// All calls made to this mock, in order. public static IReadOnlyList GetInvocations(Mock mock) where T : class - => mock.Engine.GetAllCalls(); + => GetEngine(mock).GetAllCalls(); /// Returns the mock behavior (Loose or Strict). public static MockBehavior GetBehavior(Mock mock) where T : class - => mock.Engine.Behavior; + => GetEngine(mock).Behavior; /// /// Gets the custom default value provider for unconfigured methods in loose mode. /// When set, this provider is consulted before auto-mocking and built-in defaults. /// public static IDefaultValueProvider? GetDefaultValueProvider(Mock mock) where T : class - => mock.Engine.DefaultValueProvider; + => GetEngine(mock).DefaultValueProvider; /// /// Sets the custom default value provider for unconfigured methods in loose mode. /// When set, this provider is consulted before auto-mocking and built-in defaults. /// public static void SetDefaultValueProvider(Mock mock, IDefaultValueProvider? provider) where T : class - => mock.Engine.DefaultValueProvider = provider; + => GetEngine(mock).DefaultValueProvider = provider; /// /// Enables auto-tracking for all properties. Property setters store values and getters return them, /// acting like real auto-properties. Explicit setups take precedence over auto-tracked values. /// public static void SetupAllProperties(Mock mock) where T : class - => mock.Engine.AutoTrackProperties = true; + => GetEngine(mock).AutoTrackProperties = true; /// Clears all setups and call history. public static void Reset(Mock mock) where T : class - => ((IMock)mock).Reset(); + => GetEngine(mock).Reset(); /// /// Verifies all registered setups were invoked at least once. @@ -354,13 +361,13 @@ public static void VerifyNoOtherCalls(Mock mock) where T : class /// Returns a diagnostic report of this mock's setup coverage and call matching. /// public static MockDiagnostics GetDiagnostics(Mock mock) where T : class - => mock.Engine.GetDiagnostics(); + => GetEngine(mock).GetDiagnostics(); /// /// Sets the current state for state machine mocking. Null clears the state. /// public static void SetState(Mock mock, string? stateName) where T : class - => mock.Engine.TransitionTo(stateName); + => GetEngine(mock).TransitionTo(stateName); /// /// Configures setups scoped to a specific state. All setups registered inside @@ -368,15 +375,16 @@ public static void SetState(Mock mock, string? stateName) where T : class /// public static void InState(Mock mock, string stateName, Action> configure) where T : class { - var previous = mock.Engine.PendingRequiredState; - mock.Engine.PendingRequiredState = stateName; + var engine = GetEngine(mock); + var previous = engine.PendingRequiredState; + engine.PendingRequiredState = stateName; try { configure(mock); } finally { - mock.Engine.PendingRequiredState = previous; + engine.PendingRequiredState = previous; } } } diff --git a/TUnit.Mocks/MockOfT.cs b/TUnit.Mocks/MockOfT.cs index 8c5195e55d..4f9550ca11 100644 --- a/TUnit.Mocks/MockOfT.cs +++ b/TUnit.Mocks/MockOfT.cs @@ -8,11 +8,9 @@ namespace TUnit.Mocks; /// Wraps a generated mock implementation. Source-generated extension methods on Mock<T> /// provide strongly-typed setup and verification members directly. /// -public class Mock : IMock where T : class +public class Mock : IMock, IMockEngineAccess where T : class { - /// The mock engine. Used by generated code. Not intended for direct use. - [EditorBrowsable(EditorBrowsableState.Never)] - public MockEngine Engine { get; } + private readonly MockEngine _engine; /// The mock object that implements T. public T Object { get; } @@ -20,11 +18,14 @@ public class Mock : IMock where T : class /// object IMock.ObjectInstance => Object; + /// + MockEngine IMockEngineAccess.Engine => _engine; + /// Creates a Mock wrapping the given object and engine. Used by generated code. [EditorBrowsable(EditorBrowsableState.Never)] public Mock(T mockObject, MockEngine engine) { - Engine = engine; + _engine = engine; Object = mockObject; Mock.Register(mockObject, this); } @@ -32,7 +33,7 @@ public Mock(T mockObject, MockEngine engine) /// void IMock.VerifyAll() { - var setups = Engine.GetSetups(); + var setups = _engine.GetSetups(); var uninvoked = new List(); foreach (var setup in setups) { @@ -57,7 +58,7 @@ void IMock.VerifyAll() /// void IMock.VerifyNoOtherCalls() { - var unverified = Engine.GetUnverifiedCalls(); + var unverified = _engine.GetUnverifiedCalls(); if (unverified.Count > 0) { var message = $"VerifyNoOtherCalls failed. The following calls were not verified:\n" + @@ -67,7 +68,7 @@ void IMock.VerifyNoOtherCalls() } /// - void IMock.Reset() => Engine.Reset(); + void IMock.Reset() => _engine.Reset(); /// Implicit conversion to T -- no .Object needed. public static implicit operator T(Mock mock) => mock.Object; From 030fc699a0a7931a7c7cd456abb0db03308285cc Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 25 Feb 2026 22:58:42 +0000 Subject: [PATCH 09/10] refactor(tests): cache GetInvocations result in test loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avoid repeated Mock.GetInvocations() calls in Invocations_Are_In_Call_Order test — cache the result in a local variable for clarity and efficiency. --- TUnit.Mocks.Tests/AdditionalCoverageTests.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/TUnit.Mocks.Tests/AdditionalCoverageTests.cs b/TUnit.Mocks.Tests/AdditionalCoverageTests.cs index d06ea80804..973a1cf87b 100644 --- a/TUnit.Mocks.Tests/AdditionalCoverageTests.cs +++ b/TUnit.Mocks.Tests/AdditionalCoverageTests.cs @@ -266,11 +266,12 @@ public async Task Invocations_Are_In_Call_Order() mock.Object.GetName(); mock.Object.Add(3, 4); - await Assert.That(Mock.GetInvocations(mock)).HasCount().EqualTo(4); - await Assert.That(Mock.GetInvocations(mock)[0].MemberName).IsEqualTo("Add"); - await Assert.That(Mock.GetInvocations(mock)[1].MemberName).IsEqualTo("Log"); - await Assert.That(Mock.GetInvocations(mock)[2].MemberName).IsEqualTo("GetName"); - await Assert.That(Mock.GetInvocations(mock)[3].MemberName).IsEqualTo("Add"); + var invocations = Mock.GetInvocations(mock); + await Assert.That(invocations).HasCount().EqualTo(4); + await Assert.That(invocations[0].MemberName).IsEqualTo("Add"); + await Assert.That(invocations[1].MemberName).IsEqualTo("Log"); + await Assert.That(invocations[2].MemberName).IsEqualTo("GetName"); + await Assert.That(invocations[3].MemberName).IsEqualTo("Add"); } [Test] From af8b51b8ff555d9851f5e3b625183f37183260d6 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:16:22 +0000 Subject: [PATCH 10/10] fix: address code review feedback on static Mock API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Mock.Get: use 'is' pattern match instead of direct cast to give actionable error when mock type doesn't match (distinguishes "not a mock" from "wrong mock type") - Register: remove dead try/catch in pre-.NET 7 fallback — mock objects are always unique, so Add will not throw - Register: change from public to internal — only called from Mock constructor in the same assembly - InvocationsTests: cache GetInvocations result in local variable --- TUnit.Mocks.Tests/InvocationsTests.cs | 5 +++-- TUnit.Mocks/Mock.cs | 14 ++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/TUnit.Mocks.Tests/InvocationsTests.cs b/TUnit.Mocks.Tests/InvocationsTests.cs index f44831b6d6..0e5ae4d433 100644 --- a/TUnit.Mocks.Tests/InvocationsTests.cs +++ b/TUnit.Mocks.Tests/InvocationsTests.cs @@ -32,8 +32,9 @@ public async Task Invocations_Contains_Correct_Method_Names() svc.GetValue("key1"); svc.Process(99); - await Assert.That(Mock.GetInvocations(mock)[0].MemberName).IsEqualTo("GetValue"); - await Assert.That(Mock.GetInvocations(mock)[1].MemberName).IsEqualTo("Process"); + var invocations = Mock.GetInvocations(mock); + await Assert.That(invocations[0].MemberName).IsEqualTo("GetValue"); + await Assert.That(invocations[1].MemberName).IsEqualTo("Process"); } [Test] diff --git a/TUnit.Mocks/Mock.cs b/TUnit.Mocks/Mock.cs index 856f5a1b62..2013837af9 100644 --- a/TUnit.Mocks/Mock.cs +++ b/TUnit.Mocks/Mock.cs @@ -35,16 +35,14 @@ public static class Mock /// Registers the mapping from a mock implementation object to its wrapper. /// Called from the constructor. Not intended for direct use. /// - [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] - public static void Register(object mockObject, IMock mockWrapper) + internal static void Register(object mockObject, IMock mockWrapper) { #if NET7_0_OR_GREATER _objectToMock.AddOrUpdate(mockObject, mockWrapper); #else // ConditionalWeakTable.AddOrUpdate not available before .NET 7. - // Each mock object is unique so Add should not throw, but be safe. - try { _objectToMock.Add(mockObject, mockWrapper); } - catch (ArgumentException) { _objectToMock.Remove(mockObject); _objectToMock.Add(mockObject, mockWrapper); } + // Each mock object is unique (created by new), so Add will not throw. + _objectToMock.Add(mockObject, mockWrapper); #endif } @@ -64,7 +62,11 @@ public static Mock Get(T mockedObject) where T : class { if (_objectToMock.TryGetValue(mockedObject, out var mock)) { - return (Mock)mock; + if (mock is Mock typed) + return typed; + + throw new InvalidOperationException( + $"The object is a mock of '{mock.GetType().GenericTypeArguments[0].Name}', not '{typeof(T).Name}'."); } throw new InvalidOperationException(