From 809d206d0b4967aec126f57f21963540cfd60156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sat, 1 Nov 2025 17:33:47 +0100 Subject: [PATCH 1/2] docs: improve documentation --- Docs/pages/01-mock-creation.md | 68 ++++++++++++++++++--- Docs/pages/02-setup.md | 100 +++++++++++++++++++++---------- Docs/pages/03-events.md | 33 ++++++++-- Docs/pages/04-verification.md | 106 +++++++++++++++++++++++++++++++-- Mockolate.sln | 10 ++++ 5 files changed, 266 insertions(+), 51 deletions(-) diff --git a/Docs/pages/01-mock-creation.md b/Docs/pages/01-mock-creation.md index 78892eca..f0a84d51 100644 --- a/Docs/pages/01-mock-creation.md +++ b/Docs/pages/01-mock-creation.md @@ -1,9 +1,59 @@ -# Mock Creation - -- Create mocks for interfaces and classes: - ```csharp - var mock = Mock.Create(); - var classMock = Mock.Create(); - ``` -- Provide a `MockBehavior` to control the default behavior of the mock. -- Use a `Mock.Factory` to pass a common behavior to all created mocks. +# Create mocks + +## Creating mocks for interfaces and classes + +You can create mocks for interfaces and classes. For classes without a default constructor, use `BaseClass.WithConstructorParameters(...)` to provide constructor arguments: + +```csharp +var mock = Mock.Create(); +var classMock = Mock.Create(); + +// For classes without a default constructor: +var classWithArgsMock = Mock.Create( + BaseClass.WithConstructorParameters("arg1", 42) +); +``` + +## Customizing mock behavior with `MockBehavior` + +You can control the default behavior of the mock by providing a `MockBehavior`: + +```csharp +var strictMock = Mock.Create(new MockBehavior { ThrowWhenNotSetup = true }); + +// For classes with constructor parameters and custom behavior: +var classMock = Mock.Create( + BaseClass.WithConstructorParameters("arg1", 42), + new MockBehavior { ThrowWhenNotSetup = true } +); +``` + +### MockBehavior options + +- `ThrowWhenNotSetup` (bool): + - If `true`, the mock will throw an exception when a method or property is called without a setup. + - If `false`, the mock will return a default value (see `DefaultValue`). +- `BaseClassBehavior` (enum): + - Controls how the mock interacts with base class members. Options: + - `DoNotCallBaseClass`: Do not call base class implementation (default). + - `OnlyCallBaseClass`: Only call base class implementation. + - `UseBaseClassAsDefaultValue`: Use base class as a fallback for default values. +- `DefaultValue` (IDefaultValueGenerator): + - Customizes how default values are generated for methods/properties that are not set up. + +## Using `Mock.Factory` for shared behavior + +Use `Mock.Factory` to create multiple mocks with a shared behavior: + +```csharp +var behavior = new MockBehavior { ThrowWhenNotSetup = true }; +var factory = new Mock.Factory(behavior); + +var mock1 = factory.Create(); +var mock2 = factory.Create(); +var mock3 = factory.Create(); +``` + +## Notes +- Only the first generic type can be a class; additional types must be interfaces. +- Sealed classes cannot be mocked and will throw a `MockException`. diff --git a/Docs/pages/02-setup.md b/Docs/pages/02-setup.md index a850da57..8f60cf19 100644 --- a/Docs/pages/02-setup.md +++ b/Docs/pages/02-setup.md @@ -1,66 +1,100 @@ -# Setup +# Set up mocks -Set up return values or behaviors for methods and properties on your mock. Control how the mock responds to calls in your tests. +Set up return values or behaviors for methods, properties, and indexers on your mock. Control how the mock responds to calls in your tests. + +## Method Setup + +Use `mock.Setup.Method.MethodName(...)` to set up methods. You can specify argument matchers for each parameter. -## Method setup ```csharp -mock.Setup.AddUser(With.Any()) +mock.Setup.Method.AddUser(With.Any()) .Returns(name => new User(Guid.NewGuid(), name)); + +mock.Setup.Method.TryDelete(With.Any(), With.Out(() => new User(id, "Alice"))) + .Returns(true); + +mock.Setup.Method.DoSomething(With.Matching(x => x > 0), With.Value("foo")) + .Throws(() => new InvalidOperationException()); + +mock.Setup.Method.DoWork() + .Callback(() => Console.WriteLine("Method called!")); ``` -- Use `.Callback(…)` to run code when the method is called. -- Use `.Returns(…)` to specify the value to return. You can provide a direct value or a callback to generate values on demand. -- Use `.Throws(…)` to specify an exception to throw when the method is executed. -- Use `.Returns(…)` and `.Throws(…)` repeatedly to define a sequence of return values. +- Use `.Callback(...)` to run code when the method is called. Supports parameterless or parameter callbacks. +- Use `.Returns(...)` to specify the value to return. You can provide a direct value, a callback, or a callback with parameters. +- Use `.Throws(...)` to specify an exception to throw. Supports direct exceptions, exception factories, or factories with parameters. +- Use `.Returns(...)` and `.Throws(...)` repeatedly to define a sequence of return values or exceptions (cycled on each call). + +**Async Methods** -**Argument Matching** +For `Task` or `ValueTask` methods, use `.ReturnsAsync(...)`: + +```csharp +mock.Setup.Method.GetValueAsync(With.Any()) + .ReturnsAsync(i => i * 2); +``` + +### Argument Matching Mockolate provides flexible argument matching for method setups and verifications: - `With.Any()`: Matches any value of type `T`. - `With.Matching(predicate)`: Matches values based on a predicate. -- `With.Ref(…)`/`With.Out(…)`: Matches and sets ref or out parameters. - -```csharp -mock.Setup.AddUser(With.Matching(name => name.StartsWith("A"))) - .Returns(new User(Guid.NewGuid(), "Alicia")); - -mock.Setup.TryDelete(With.Any(), With.Out(() => new User(id, "Alice"))) - .Returns(true); -``` +- `With.Value(value)`: Matches a specific value. +- `With.Null()`: Matches null. +- `With.Out(...)`/`With.Ref(...)`: Matches and sets out/ref parameters, supports value setting and predicates. +- For .NET 8+: `With.ValueBetween(min).And(max)` matches a range (numeric types). ## Property Setup Set up property getters and setters to control or verify property access on your mocks. Supports auto-properties and indexers. -**Initialization** -You can initialize properties and they will work like normal properties (setter changes the value, getter returns the last set value). +**Initialization** + +You can initialize properties so they work like normal properties (setter changes the value, getter returns the last set value): ```csharp mock.Setup.Property.MyProperty.InitializeWith(42); ``` -**Returns / Throws** -Alternatively you can set up the properties similar to methods with `Returns` and `Throws`. +**Returns / Throws** + +Alternatively, set up properties with `Returns` and `Throws` (supports sequences): + ```csharp mock.Setup.Property.MyProperty - .Returns(1) - .Returns(2) - .Throws(new Exception("Error")) - .Returns(4); + .Returns(1) + .Returns(2) + .Throws(new Exception("Error")) + .Returns(4); ``` -**Callbacks** -Callbacks can be registered on the setter or getter. +**Callbacks** + +Register callbacks on the setter or getter: + ```csharp mock.Setup.Property.MyProperty.OnGet(() => Console.WriteLine("MyProperty was read!")); -mock.Setup.Property.MyProperty.OnSet(value => Console.WriteLine($"Set MyProperty to {value}!")); +mock.Setup.Property.MyProperty.OnSet((oldValue, newValue) => Console.WriteLine($"Changed from {oldValue} to {newValue}!")); ``` -**Indexers** -Indexers are supported as well. +## Indexer Setup + +Set up indexers with argument matchers. Supports initialization, returns/throws sequences, and callbacks. + ```csharp mock.Setup.Indexer(With.Any()) - .InitializeWith(index => index*index) - .OnGet(index => Console.WriteLine($"Indexer this[{index}] was read")); + .InitializeWith(index => index * index) + .Returns((v, index) => v + 1) + .OnGet(index => Console.WriteLine($"Indexer this[{index}] was read")); + +mock.Setup.Indexer(With.Any(), With.Matching(i => i > 0)) + .InitializeWith((i, j) => $"init-{i}-{j}") + .Returns((v, i, j) => $"{v}-{i}-{j}") + .OnSet((value, i, j) => Console.WriteLine($"Set [{i},{j}] to {value}")); ``` + +- `.InitializeWith(...)` can take a value or a callback with parameters. +- `.Returns(...)` and `.Throws(...)` support direct values, callbacks, and callbacks with parameters and/or the current value. +- `.OnGet(...)` and `.OnSet(...)` support callbacks with or without parameters. +- `.Returns(...)` and `.Throws(...)` can be chained to define a sequence of behaviors, which are cycled through on each call. diff --git a/Docs/pages/03-events.md b/Docs/pages/03-events.md index 8e88b672..11f3d8f5 100644 --- a/Docs/pages/03-events.md +++ b/Docs/pages/03-events.md @@ -1,10 +1,33 @@ -# Event Raising +# Mock events -Easily raise events on your mock to test event handlers in your code: +Easily raise events on your mock to test event handlers in your code. + +## Usage + +Use the strongly-typed `Raise` property on your mock to trigger events declared on the mocked interface or class. The method signature matches the event delegate. + +```csharp +// Arrange: subscribe a handler to the event +mock.Subject.UsersChanged += (sender, args) => { /* handler code */ }; + +// Act: raise the event +mock.Raise.UsersChanged(this, EventArgs.Empty); +``` + +- Use the `Raise` property to trigger events declared on the mocked interface or class. +- Only currently subscribed handlers will be invoked. +- Simulate notifications and test event-driven logic in your code. + +## Example ```csharp -mock.Raises.UsersChanged(this, EventArgs.Empty); +int callCount = 0; +mock.Subject.UsersChanged += (sender, args) => callCount++; + +mock.Raise.UsersChanged(this, EventArgs.Empty); +mock.Raise.UsersChanged(this, EventArgs.Empty); + +// callCount == 2 ``` -- Use the `Raises` property to trigger events declared on the mocked interface or class. -- Simulate notifications and test event-driven logic. +You can subscribe and unsubscribe handlers as needed. Only handlers subscribed at the time of raising the event will be called. diff --git a/Docs/pages/04-verification.md b/Docs/pages/04-verification.md index 53993a68..09df8f58 100644 --- a/Docs/pages/04-verification.md +++ b/Docs/pages/04-verification.md @@ -1,18 +1,96 @@ -# Verification +# Verify mock interactions -Verify that methods or properties were called with specific arguments and how many times: +You can verify that methods, properties, indexers, or events were called or accessed with specific arguments and how many times, using the `Verify` API: + +Supported call count verifications in the `Mockolate.Verify` namespace: +- `.Never()` +- `.Once()` +- `.Twice()` +- `.Exactly(n)` +- `.AtLeastOnce()` +- `.AtLeastTwice()` +- `.AtLeast(n)` +- `.AtMostOnce()` +- `.AtMostTwice()` +- `.AtMost(n)` + +## Methods + +You can verify that methods were called with specific arguments and how many times: ```csharp +// Verify that AddUser("Bob") was called at least once mock.Verify.Invoked.AddUser("Bob").AtLeastOnce(); + +// Verify that TryDelete was never called with the given id and any out parameter mock.Verify.Invoked.TryDelete(id, With.Out()).Never(); + +// Verify that DoSomething was called exactly twice with any int argument mock.Verify.Invoked.DoSomething(With.Any()).Exactly(2); ``` -- Supports `.Never()`, `Once()`, `Twice()`, `Exactly(n)`, `.AtLeastOnce()`, `.AtLeastTwice()`, `.AtLeast(n)`, `.AtMostOnce()`, `.AtMostTwice()`, `.AtMost(n)` for call count verification. -- Verify arguments with matchers. +### Argument Matchers + +You can use argument matchers from the `With` class to verify calls with flexible conditions: + +- `With.Any()` — matches any value of type `T` +- `With.Null()` — matches `null` +- `With.Matching(predicate)` — matches values satisfying a predicate +- `With.Value(value)` — matches a specific value +- `With.Out()` — matches any out parameter of type `T` +- `With.Ref()` — matches any ref parameter of type `T` +- `With.Out(setter)` — matches and sets an out parameter +- `With.Ref(setter)` — matches and sets a ref parameter +- `With.ValueBetween(min).And(max)` — matches a value between min and max (for numeric types, .NET 8+) + +Example: +```csharp +mock.Verify.Invoked.DoSomething(With.Matching(x => x > 10)).Once(); +mock.Verify.Invoked.DoSomething(With.ValueBetween(1).And(5)).AtLeastOnce(); +``` + +## Properties +You can verify property gets and sets: + +```csharp +// Verify that the property 'Name' was read at least once +mock.Verify.Got.Name().AtLeastOnce(); + +// Verify that the property 'Age' was set to 42 exactly once +mock.Verify.Set.Age(42).Once(); +``` + +Note: The setter value also supports argument matchers. + +## Indexers + +You can verify indexer gets and sets: + +```csharp +// Verify that the indexer was read with key "foo" exactly once +mock.Verify.GotIndexer("foo").Once(); + +// Verify that the indexer was set with key "bar" to value 123 at least once +mock.Verify.SetIndexer("bar", 123).AtLeastOnce(); +``` + +Note: The keys and value also supports argument matchers. + +## Events + +You can verify event subscriptions and unsubscriptions: + +```csharp +// Verify that the event 'Changed' was subscribed to at least once +mock.Verify.SubscribedTo.Changed().AtLeastOnce(); + +// Verify that the event 'Changed' was unsubscribed from exactly once +mock.Verify.UnsubscribedFrom.Changed().Once(); +``` ## Call Ordering + Use `Then` to verify that calls occurred in a specific order: ```csharp @@ -20,3 +98,23 @@ mock.Verify.Invoked.AddUser("Alice").Then( m => m.Invoked.DeleteUser("Alice") ); ``` + +You can chain multiple calls for strict order verification: + +```csharp +mock.Verify.Invoked.DoSomething(1) + .Then(m => m.Invoked.DoSomething(2), m => m.Invoked.DoSomething(3)); +``` + +If the order is incorrect or a call is missing, a `MockVerificationException` will be thrown with a descriptive message. + +## Verifying All Interactions + +You can check if all interactions with the mock have been verified using `ThatAllInteractionsAreVerified`: + +```csharp +// Returns true if all interactions have been verified before +bool allVerified = mock.Verify.ThatAllInteractionsAreVerified(); +``` + +This is useful for ensuring that your test covers all interactions and that no unexpected calls were made. If any interaction was not verified, this method returns `false`. diff --git a/Mockolate.sln b/Mockolate.sln index 0efba081..1c76f27a 100644 --- a/Mockolate.sln +++ b/Mockolate.sln @@ -75,6 +75,15 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mockolate.Analyzers.CodeFix EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mockolate.Analyzers.Tests", "Tests\Mockolate.Analyzers.Tests\Mockolate.Analyzers.Tests.csproj", "{59D8A688-90F2-9C76-39EA-D334626F3D00}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{998DB807-840E-4E00-956E-350FDA8C851B}" + ProjectSection(SolutionItems) = preProject + Docs\pages\00-index.md = Docs\pages\00-index.md + Docs\pages\01-mock-creation.md = Docs\pages\01-mock-creation.md + Docs\pages\02-setup.md = Docs\pages\02-setup.md + Docs\pages\03-events.md = Docs\pages\03-events.md + Docs\pages\04-verification.md = Docs\pages\04-verification.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -145,6 +154,7 @@ Global {D8D97888-0AE8-4558-8CAC-0C2A3E7F9B08} = {17A2EFA6-036A-4555-B74D-EDECCEA6072D} {17A2EFA6-036A-4555-B74D-EDECCEA6072D} = {9CC57AD0-4984-4618-96EA-01FFFCCD84FA} {59D8A688-90F2-9C76-39EA-D334626F3D00} = {9CC57AD0-4984-4618-96EA-01FFFCCD84FA} + {998DB807-840E-4E00-956E-350FDA8C851B} = {99D141BC-CCD6-4EEF-99DA-828CEF1F8928} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8D05DD33-55B6-4A6D-BB7A-68C505BBA67E} From 5c58992378fd9ccea8d67b92415c0a7650685dbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sat, 1 Nov 2025 18:24:14 +0100 Subject: [PATCH 2/2] Review issues --- Docs/pages/02-setup.md | 2 +- Docs/pages/04-verification.md | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Docs/pages/02-setup.md b/Docs/pages/02-setup.md index 8f60cf19..39f06b98 100644 --- a/Docs/pages/02-setup.md +++ b/Docs/pages/02-setup.md @@ -13,7 +13,7 @@ mock.Setup.Method.AddUser(With.Any()) mock.Setup.Method.TryDelete(With.Any(), With.Out(() => new User(id, "Alice"))) .Returns(true); -mock.Setup.Method.DoSomething(With.Matching(x => x > 0), With.Value("foo")) +mock.Setup.Method.DoSomething(With.Matching(x => x > 0)) .Throws(() => new InvalidOperationException()); mock.Setup.Method.DoWork() diff --git a/Docs/pages/04-verification.md b/Docs/pages/04-verification.md index 09df8f58..cca69d7a 100644 --- a/Docs/pages/04-verification.md +++ b/Docs/pages/04-verification.md @@ -82,11 +82,11 @@ Note: The keys and value also supports argument matchers. You can verify event subscriptions and unsubscriptions: ```csharp -// Verify that the event 'Changed' was subscribed to at least once -mock.Verify.SubscribedTo.Changed().AtLeastOnce(); +// Verify that the event 'UsersChanged' was subscribed to at least once +mock.Verify.SubscribedTo.UsersChanged().AtLeastOnce(); -// Verify that the event 'Changed' was unsubscribed from exactly once -mock.Verify.UnsubscribedFrom.Changed().Once(); +// Verify that the event 'UsersChanged' was unsubscribed from exactly once +mock.Verify.UnsubscribedFrom.UsersChanged().Once(); ``` ## Call Ordering @@ -102,8 +102,9 @@ mock.Verify.Invoked.AddUser("Alice").Then( You can chain multiple calls for strict order verification: ```csharp -mock.Verify.Invoked.DoSomething(1) - .Then(m => m.Invoked.DoSomething(2), m => m.Invoked.DoSomething(3)); +mock.Verify.Invoked.DoSomething(1).Then( + m => m.Invoked.DoSomething(2), + m => m.Invoked.DoSomething(3)); ``` If the order is incorrect or a call is missing, a `MockVerificationException` will be thrown with a descriptive message.