From dbb5cde3271617247fae1809e14ae1c283268acb Mon Sep 17 00:00:00 2001 From: Robert Coltheart Date: Sun, 31 May 2026 08:29:08 +1000 Subject: [PATCH 1/2] Add WasCalled to tunit mocks assertions --- TUnit.Mocks.Assertions/MockAssertionExtensions.cs | 10 ++++++++++ TUnit.Mocks.Tests/AsyncVerificationTests.cs | 14 ++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/TUnit.Mocks.Assertions/MockAssertionExtensions.cs b/TUnit.Mocks.Assertions/MockAssertionExtensions.cs index 98dd817347..3fcccdbdd7 100644 --- a/TUnit.Mocks.Assertions/MockAssertionExtensions.cs +++ b/TUnit.Mocks.Assertions/MockAssertionExtensions.cs @@ -10,6 +10,16 @@ namespace TUnit.Mocks.Assertions; /// public static class MockAssertionExtensions { + /// + /// Asserts that the mock member was called at least once. + /// + public static WasCalledAssertion WasCalled( + this IAssertionSource source) + { + source.Context.ExpressionBuilder.Append($".WasCalled({nameof(Times.AtLeastOnce)})"); + return new WasCalledAssertion(source.Context, Times.AtLeastOnce); + } + /// /// Asserts that the mock member was called the specified number of times. /// diff --git a/TUnit.Mocks.Tests/AsyncVerificationTests.cs b/TUnit.Mocks.Tests/AsyncVerificationTests.cs index 055094109f..fecf6d6e0d 100644 --- a/TUnit.Mocks.Tests/AsyncVerificationTests.cs +++ b/TUnit.Mocks.Tests/AsyncVerificationTests.cs @@ -84,6 +84,20 @@ await Assert.That(mock.Add(Any(), Any())) .WasCalled(Times.AtLeastOnce); } + [Test] + public async Task WasCalled_AtLeastOnceByDefault_Passes() + { + var mock = ICalculator.Mock(); + mock.Add(Any(), Any()).Returns(42); + + ICalculator calc = mock.Object; + _ = calc.Add(1, 2); + _ = calc.Add(3, 4); + + await Assert.That(mock.Add(Any(), Any())) + .WasCalled(); + } + [Test] public async Task Property_Getter_WasCalled_Via_Assert() { From fb248187461eb3a7da24b1b36baf3243eef95414 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 31 May 2026 15:20:10 +0100 Subject: [PATCH 2/2] refactor(assertions): generate WasCalled() via [GenerateAssertion] Replace the hand-written parameterless WasCalled() extension with a [GenerateAssertion]-decorated method, per the project's preference for generated assertions. The previous manual overload targeted IAssertionSource directly, which does not bind to the concrete IAssertionSource produced by Assert.That(mock.Method()) (IAssertionSource is invariant), so the WasCalled() call site did not compile. The generated covariant form WasCalled(this IAssertionSource) where TActual : ICallVerification binds correctly. - Declare the source method as a non-extension static so the generator emits a static-qualified call, avoiding the name clash with the void ICallVerification.WasCalled() instance method. - Reference TUnit.Assertions.SourceGenerator as an analyzer (it does not flow transitively through the TUnit.Assertions project reference). - Add a failing-path test for the parameterless WasCalled(). --- .../MockAssertionExtensions.cs | 23 +++++++++++++++---- .../TUnit.Mocks.Assertions.csproj | 1 + TUnit.Mocks.Tests/AsyncVerificationTests.cs | 10 ++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/TUnit.Mocks.Assertions/MockAssertionExtensions.cs b/TUnit.Mocks.Assertions/MockAssertionExtensions.cs index 3fcccdbdd7..a9214990aa 100644 --- a/TUnit.Mocks.Assertions/MockAssertionExtensions.cs +++ b/TUnit.Mocks.Assertions/MockAssertionExtensions.cs @@ -1,5 +1,7 @@ using System.Runtime.CompilerServices; +using TUnit.Assertions.Attributes; using TUnit.Assertions.Core; +using TUnit.Mocks.Exceptions; using TUnit.Mocks.Verification; namespace TUnit.Mocks.Assertions; @@ -12,12 +14,25 @@ public static class MockAssertionExtensions { /// /// Asserts that the mock member was called at least once. + /// Generates the WasCalled() assertion on . /// - public static WasCalledAssertion WasCalled( - this IAssertionSource source) + [GenerateAssertion(ExpectationMessage = "to have been called")] + public static AssertionResult WasCalled(ICallVerification verification) { - source.Context.ExpressionBuilder.Append($".WasCalled({nameof(Times.AtLeastOnce)})"); - return new WasCalledAssertion(source.Context, Times.AtLeastOnce); + if (verification is null) + { + return AssertionResult.Failed("Verification target is null"); + } + + try + { + verification.WasCalled(); + return AssertionResult.Passed; + } + catch (MockVerificationException ex) + { + return AssertionResult.Failed(ex.Message); + } } /// diff --git a/TUnit.Mocks.Assertions/TUnit.Mocks.Assertions.csproj b/TUnit.Mocks.Assertions/TUnit.Mocks.Assertions.csproj index ec69d4e884..e0deaada1f 100644 --- a/TUnit.Mocks.Assertions/TUnit.Mocks.Assertions.csproj +++ b/TUnit.Mocks.Assertions/TUnit.Mocks.Assertions.csproj @@ -9,6 +9,7 @@ + diff --git a/TUnit.Mocks.Tests/AsyncVerificationTests.cs b/TUnit.Mocks.Tests/AsyncVerificationTests.cs index fecf6d6e0d..af4b6974d0 100644 --- a/TUnit.Mocks.Tests/AsyncVerificationTests.cs +++ b/TUnit.Mocks.Tests/AsyncVerificationTests.cs @@ -98,6 +98,16 @@ await Assert.That(mock.Add(Any(), Any())) .WasCalled(); } + [Test] + public async Task WasCalled_Default_Fails_When_Not_Called() + { + var mock = ICalculator.Mock(); + + await Assert.ThrowsAsync(async () => + await Assert.That(mock.Add(Any(), Any())) + .WasCalled()); + } + [Test] public async Task Property_Getter_WasCalled_Via_Assert() {