diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4d6c6baa..d982e769 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -214,8 +214,8 @@ The project uses GitHub Actions with several workflows: ## Documentation -- Keep README.md up to date with API changes -- Update markdown files under `Docs/` to match README.md content - they are published to https://awexpect.com/docs/mockolate/index +- README is the GitHub/NuGet landing page (pitch + quick start + links to docs); full reference docs live under `Docs/pages/` and are published to https://docs.testably.org/Mockolate/ +- Keep `Docs/pages/` up to date with API changes; the README's link index should match the published structure - Document public APIs with XML comments - Examples should be in Mockolate.ExampleTests - Follow existing documentation style diff --git a/CLAUDE.md b/CLAUDE.md index 7e6635f2..503c7d88 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -114,5 +114,5 @@ All PRs must pass: - New public API must have XML doc comments - All assemblies are **strong-named** (`Directory.Build.props`) - Package versions managed centrally in `Directory.Packages.props` -- Documentation lives in `Docs/pages/` and mirrors the README; published at https://awexpect.com/docs/mockolate/index +- Documentation lives in `Docs/pages/`; the README links to it for reference content. Published at https://docs.testably.org/Mockolate/ - Examples go in `Mockolate.ExampleTests`, not inline in test helper files \ No newline at end of file diff --git a/Docs/pages/setup/04-parameter-matching.md b/Docs/pages/setup/04-parameter-matching.md index b00bde23..875fb900 100644 --- a/Docs/pages/setup/04-parameter-matching.md +++ b/Docs/pages/setup/04-parameter-matching.md @@ -124,7 +124,7 @@ int result = sut.Process("HELLO"); Parameter matchers are covariant in their type argument: when a method declares a parameter of a base type, you can narrow the match by supplying a matcher for a derived type. Only calls whose actual argument is an instance of that -derived type (or a further-derived type) will match — calls passing other runtime types fall through to other setups. +derived type (or a further-derived type) match the setup; calls passing other runtime types fall through. ```csharp public abstract class Chocolate { } @@ -144,7 +144,7 @@ sut.Mock.Setup.Add(It.IsAny()).Returns(true); bool dark = sut.Add(new DarkChocolate()); // true, matched the DarkChocolate setup bool milk = sut.Add(new MilkChocolate()); // false, no setup matched -> default -// Verifications are covariant too — only dark-chocolate additions are counted. +// Verifications are covariant too: only dark-chocolate additions are counted. sut.Mock.Verify.Add(It.IsAny()).Once(); ``` diff --git a/README.md b/README.md index c60679fa..3f4dfeea 100644 --- a/README.md +++ b/README.md @@ -27,1726 +27,52 @@ It enables fast, compile-time validated mocking with .NET Standard 2.0, .NET 8, | Hot path | dynamic-proxy dispatch | direct dispatch | For side-by-side setup, usage, and verification syntax against Moq, NSubstitute, and FakeItEasy, see the -[full code comparison](https://docs.testably.org/mockolate/comparison). +[full code comparison](https://docs.testably.org/Mockolate/comparison); for performance, see the +[benchmarks](https://docs.testably.org/Mockolate/benchmarks). Already on Moq or NSubstitute? The companion package [`Mockolate.Migration`](https://github.com/Testably/Mockolate.Migration) ships analyzers and code fixers that translate common Moq and NSubstitute patterns to Mockolate syntax in-place: point it at an existing test project and apply the suggested fixes. -## Getting Started +## Installation -1. **Check prerequisites** - Install the [.NET 10 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/10.0), because Mockolate leverages - C# 14 extension members (the projects can still target any supported framework). +Install the [.NET 10 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/10.0). Mockolate leverages C# 14 +extension members (the projects can still target any supported framework). Then add the package: -2. **Install the package** - ```powershell - dotnet add package Mockolate - ``` - -3. **Create and use a mock** - ```csharp - using Mockolate; - - public interface IChocolateDispenser - { - bool Dispense(string type, int amount); - } - - // Create a mock - IChocolateDispenser sut = IChocolateDispenser.CreateMock(); - - // Setup: Dispense returns true for any Dark chocolate request - sut.Mock.Setup.Dispense("Dark", It.IsAny()).Returns(true); - - // Act - bool success = sut.Dispense("Dark", 4); - - // Verify - sut.Mock.Verify.Dispense("Dark", It.IsAny()).Once(); - ``` - - For a richer walkthrough combining properties, indexers, events, and stateful setup, - see [A complete example](#a-complete-example) at the end of the README. - -## Create mocks - -You can create mocks for interfaces and classes. For classes without a default constructor, provide constructor -arguments as an array to `CreateMock([…])`: - -```csharp -// Create a mock of an interface -IChocolateDispenser sut = IChocolateDispenser.CreateMock(); - -// Create a mock of a class -MyChocolateDispenser classMock = MyChocolateDispenser.CreateMock(); - -// For classes without a default constructor: -MyChocolateDispenserWithCtor classWithArgsMock = MyChocolateDispenserWithCtor.CreateMock("Dark", 42); -``` - -### Customizing mock behavior - -You can control the default behavior of the mock by providing a `MockBehavior`: - -```csharp -IChocolateDispenser strictMock = IChocolateDispenser.CreateMock(MockBehavior.Default.ThrowingWhenNotSetup()); - -// For classes with constructor parameters and custom behavior: -MockBehavior behavior = new MockBehavior { ThrowWhenNotSetup = true }; -MyChocolateDispenser classMock = MyChocolateDispenser.CreateMock(behavior, "Dark", 42); -``` - -**`MockBehavior` options** - -| Option | Default | Purpose | -|---------------------------------------|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `SkipBaseClass` | `false` | When `true`, the mock does not call any base class implementations. Otherwise, the base class implementation is used as the default value when no explicit setup matches. | -| `ThrowWhenNotSetup` | `false` | When `true`, the mock throws when no matching setup is found. Otherwise, it returns a default value (see `DefaultValue` below). | -| `SkipInteractionRecording` | `false` | When `true`, interactions are not recorded - setups, returns, callbacks, and base-class delegation still work, but `.Verify.X()` throws a `MockException`. Useful in performance-sensitive scenarios. | -| `DefaultValue` | sensible defaults | Customizes how default values are generated for unset methods and properties (see below). | -| `Initialize(...)` | - | Automatically applies the given setups to all mocks of type `T` when they are created. | -| `UseConstructorParametersFor(...)` | - | Configures default constructor parameters for mocks of type `T`, unless explicit parameters are supplied to `CreateMock([…])`. The `Func` overload defers parameter resolution until each mock is created. | - -**Default value generation** - -The default `IDefaultValueGenerator` provides sensible defaults for the most common cases: - -- Empty collections for collection types (e.g., `IEnumerable`, `List`) -- Empty string for `string` -- Completed tasks for `Task`, `Task`, `ValueTask`, and `ValueTask` -- Tuples with recursively defaulted values -- `null` for other reference types - -You can register custom factories per type using `.WithDefaultValueFor()`: - -```csharp -MockBehavior behavior = MockBehavior.Default - .WithDefaultValueFor(() => "default") - .WithDefaultValueFor(() => 42); -IChocolateDispenser sut = IChocolateDispenser.CreateMock(behavior); -``` - -For full control, implement `IDefaultValueGenerator` directly and assign it to `MockBehavior.DefaultValue`. - -**Using a shared behavior** - -You can reuse a `MockBehavior` instance across multiple mock creations to apply consistent, centrally configured -behavior: - -```csharp -MockBehavior behavior = MockBehavior.Default.ThrowingWhenNotSetup(); - -IChocolateDispenser sut1 = IChocolateDispenser.CreateMock(behavior); -ILemonadeDispenser sut2 = ILemonadeDispenser.CreateMock(behavior); -``` - -This is especially useful when you need consistent mock setups across multiple tests or for different types. - -### Setups - -Specify setups during mock creation using the `CreateMock` overload with a setup callback. These setups also apply to -virtual interactions in the constructor. - -```csharp -IChocolateDispenser sut = IChocolateDispenser.CreateMock(setup => -{ - setup.Dispense(It.IsAny(), It.IsAny()).Returns(true); - setup.TotalDispensed.InitializeWith(0); -}); -``` - -You can combine the setup callback with a `MockBehavior` and constructor parameters in the same call. - -### Implementing additional interfaces - -You can specify additional interfaces that the mock also implements using `.Implementing()`: - -```csharp -// return type is a MyChocolateDispenser that also implements ILemonadeDispenser -MyChocolateDispenser sut = MyChocolateDispenser.CreateMock().Implementing(); - -// Create a mock implementing multiple interfaces with inline setups -IChocolateDispenser sut2 = IChocolateDispenser.CreateMock() - .Implementing(setup => setup.DispenseLemonade(It.IsAny()).Returns(true)); -``` - -**Accessing the additional interface's mock surface** - -Use `Mock.As()` to reach the `Setup` and `Verify` properties for an additional interface added via -`.Implementing()`: - -```csharp -MyChocolateDispenser sut = MyChocolateDispenser.CreateMock() - .Implementing(); - -// Set up and verify members of the additional interface -sut.Mock.As().Setup.DispenseLemonade(It.IsAny()).Returns(true); -sut.Mock.As().Verify.DispenseLemonade(5).Once(); -``` - -The returned mock shares the registry of the original - recorded interactions, scenario state, and setups apply -across all faces of the same instance. - -**Notes:** - -- Only the first type can be a class; additional types must be interfaces. - -### Wrapping existing instances - -You can wrap an existing instance with mock tracking using `.Wrapping(instance)`. This allows you to track interactions -with a real object: - -```csharp -MyChocolateDispenser realDispenser = new MyChocolateDispenser(); -IChocolateDispenser wrappedDispenser = IChocolateDispenser.CreateMock().Wrapping(realDispenser); - -// Calls are forwarded to the real instance -wrappedDispenser.Dispense("Dark", 5); - -// But you can still verify interactions -wrappedDispenser.Mock.Verify.Dispense(It.Is("Dark"), It.Is(5)).Once(); -``` - -**Notes:** - -- Both interface and class types can be wrapped. -- All public calls are forwarded to the wrapped instance. -- You can still set up custom behavior that overrides the wrapped instance's behavior. -- Protected members are not forwarded to the wrapped instance; the base class implementation is used instead. -- Verification works the same as with regular mocks. - -## Setup - -Set up return values or behaviors for methods, properties, and indexers on your mock. Control how the mock responds to -calls in your tests. - -### Properties - -Set up property getters and setters to control or verify property access on your mocks. - -**Initialization** - -You can initialize properties so they work like normal properties (setter changes the value, getter returns the last set -value): - -```csharp -sut.Mock.Setup.TotalDispensed.InitializeWith(42); -``` - -You can also register a setup without providing a value (useful when `ThrowWhenNotSetup` is enabled): - -```csharp -IChocolateDispenser sut = IChocolateDispenser.CreateMock(MockBehavior.Default.ThrowingWhenNotSetup()); - -// Register property without value - won't throw -sut.Mock.Setup.TotalDispensed.Register(); -``` - -**Returns / Throws** - -Set up properties with `Returns` and `Throws` (supports sequences): - -```csharp -sut.Mock.Setup.TotalDispensed - .Returns(1) - .Returns(2) - .Throws(new Exception("Error")) - .Returns(4); -``` - -You can also return a value based on the previous value: - -```csharp -sut.Mock.Setup.TotalDispensed - .Returns(current => current + 10); // Increment by 10 each read -``` - -**Callbacks** - -Register callbacks on the setter or getter: - -```csharp -sut.Mock.Setup.TotalDispensed.OnGet - .Do(() => Console.WriteLine("TotalDispensed was read!")); -sut.Mock.Setup.TotalDispensed.OnSet - .Do(newValue => Console.WriteLine($"Changed to {newValue}!") ); -``` - -Callbacks can also receive the current value: - -```csharp -// Getter with the current value -sut.Mock.Setup.TotalDispensed - .OnGet.Do(value => - Console.WriteLine($"Read TotalDispensed current value: {value}")); - -// Setter with the new value -sut.Mock.Setup.TotalDispensed - .OnSet.Do(newValue => - Console.WriteLine($"Set TotalDispensed to {newValue}")); -``` - -Callbacks also support sequences, similar to `Returns` and `Throws`: - -```csharp -sut.Mock.Setup.TotalDispensed.OnGet - .Do(() => Console.WriteLine("Execute on all even read interactions")) - .Do(() => Console.WriteLine("Execute on all odd read interactions")); -``` - -*Notes:* - -- Use `.SkippingBaseClass(…)` to override the base class behavior for a specific property (only for class mocks). -- All callbacks and return values support more advanced features like conditional execution, frequency control, - parallel execution, and access to the invocation counter. - See [Advanced callback features](#advanced-callback-features) for details. - -### Methods - -Use `sut.Mock.Setup.MethodName(…)` to set up methods. You can specify argument matchers for each parameter. - -**Returns / Throws** - -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. - -You can call `.Returns(…)` and `.Throws(…)` multiple times to define a sequence of return values or exceptions (cycled -on each call). - -```csharp -// Setup Dispense to decrease stock and raise event -sut.Mock.Setup.Dispense(It.Is("Dark"), It.IsAny()) - .Returns((type, amount) => - { - var current = sut[type]; - if (current >= amount) - { - sut[type] = current - amount; - sut.Mock.Raise.ChocolateDispensed(type, amount); - return true; - } - return false; - }); - -// Setup method to throw -sut.Mock.Setup.Dispense(It.Is("Green"), It.IsAny()) - .Throws(); - -// Sequence of returns and throws -sut.Mock.Setup.Dispense(It.IsAny(), It.IsAny()) - .Returns(true) - .Throws(new Exception("Error")) - .Returns(false); -``` - -**Async Methods** - -For async methods returning `Task`/`Task` or `ValueTask`/`ValueTask`, use `.ReturnsAsync(…)` or `ThrowsAsync(…)` -respectively: - -```csharp -sut.Mock.Setup.DispenseAsync(It.IsAny(), It.IsAny()) - .ReturnsAsync((_, v) => v) // First execution returns the value of the `int` parameter - .ThrowsAsync(new TimeoutException()) // Second execution throws a TimeoutException - .ReturnsAsync(0).Forever(); // Subsequent executions return 0 -``` - -**Callbacks** - -Use `.Do(…)` to run code when the method is called. Supports parameterless or parameter callbacks. - -```csharp -// Setup method with callback -sut.Mock.Setup.Dispense(It.Is("White"), It.IsAny()) - .Do((type, amount) => Console.WriteLine($"Dispensed {amount} {type} chocolate.")); -``` - -*Notes:* - -- Use `.SkippingBaseClass(…)` to override the base class behavior for a specific method (only for class mocks). -- When you specify overlapping setups, the most recently defined setup takes precedence. -- All callbacks and return values support more advanced features like conditional execution, frequency control, - parallel execution, and access to the invocation counter. - See [Advanced callback features](#advanced-callback-features) for details. - -### Indexers - -Set up indexers with argument matchers. Supports initialization, returns/throws sequences, and callbacks. - -```csharp -sut.Mock.Setup[It.IsAny()] - .InitializeWith(type => 20) - .OnGet.Do(type => Console.WriteLine($"Stock for {type} was read")); - -sut.Mock.Setup[It.Is("Dark")] - .InitializeWith(10) - .OnSet.Do((type, value) => Console.WriteLine($"Set [{type}] to {value}")); -``` - -**Initialization** - -You can initialize indexers so they work like normal indexers (setter changes the value, getter returns the last set -value): - -```csharp -sut.Mock.Setup[It.IsAny()].InitializeWith(42); -``` - -**Returns / Throws** - -Set up indexers with `Returns` and `Throws` (supports sequences): - -```csharp -sut.Mock.Setup[It.IsAny()] - .Returns(1) - .Returns(2) - .Throws(new Exception("Error")) - .Returns(4); -``` - -You can also return a value based on the previous value: - -```csharp -sut.Mock.Setup[It.IsAny()] - .Returns(current => current + 10); // Increment by 10 each read -``` - -**Callbacks** - -Register callbacks on the setter or getter of the indexer: - -```csharp -sut.Mock.Setup[It.IsAny()].OnGet - .Do(() => Console.WriteLine("Indexer was read!")); -sut.Mock.Setup[It.IsAny()].OnSet - .Do(newValue => Console.WriteLine($"Changed indexer to {newValue}!") ); -``` - -Callbacks can also receive the indexer parameters and the current value: - -```csharp -// Getter with the current value -sut.Mock.Setup[It.IsAny()] - .OnGet.Do((string index, int value) => - Console.WriteLine($"Read this[{index}] current value: {value}")); - -// Setter with the new value -sut.Mock.Setup[It.IsAny()] - .OnSet.Do((string index, int newValue) => - Console.WriteLine($"Set this[{index}] to {newValue}")); -``` - -Callbacks also support sequences, similar to `Returns` and `Throws`: - -```csharp -sut.Mock.Setup[It.IsAny()].OnGet - .Do(() => Console.WriteLine("Execute on all even read interactions")) - .Do(() => Console.WriteLine("Execute on all odd read interactions")); -``` - -**Notes:** - -- All callbacks support more advanced features like conditional execution, frequency control, parallel execution, and - access to the invocation counter. - See [Advanced callback features](#advanced-callback-features) for - details. -- You can use the same [parameter matching](#parameter-matching) - and [interaction](#parameter-interaction) options as for - methods. -- Use `.SkippingBaseClass(…)` to override the base class behavior for a specific indexer (only for class mocks). -- When you specify overlapping setups, the most recently defined setup takes precedence. - -### Parameter Matching - -Mockolate provides flexible parameter matching for method setups and verifications: - -#### Parameter Matchers - -**Basic Matchers** - -- `It.IsAny()`: Matches any value of type `T`. -- `It.Is(value)`: Matches a specific value. -- `It.IsNot(value)`: Matches any value not equal to `value`. -- `It.IsOneOf(params IEnumerable values)`: Matches any of the given values. -- `It.IsNotOneOf(params IEnumerable values)`: Matches any value that is not in the given set. -- `It.IsNull()`: Matches null. -- `It.IsNotNull()`: Matches any non-null value. -- `It.IsTrue()`/`It.IsFalse()`: Matches boolean true/false. -- `It.IsInRange(min, max)`: Matches a number within the given range. You can append `.Exclusive()` to exclude the - minimum and maximum value. -- `It.Satisfies(predicate)`: Matches values based on a predicate. - -*Note:* -You can also directly use a value (equivalent to wrapping it in `It.Is(value)`). For methods and indexers with -up to 4 parameters, you can freely mix matchers with direct values in any position. For members with more than -4 parameters, Mockolate still supports explicit values, but limits arbitrary per-parameter mixing to avoid a -combinatorial explosion of overloads. You may need to use direct values for all explicit-value-capable parameters -and wrap the remaining ones, or use matchers for all parameters. - -**String Matching** - -- `It.Matches(pattern)`: Matches strings using wildcard patterns (`*` and `?`). - -**Regular Expressions** -Use `.AsRegex()` to enable regular expression matching for `It.Matches()`: - -```csharp -// Example: Match email addresses -sut.Mock.Setup.ValidateEmail(It.Matches(@"^\w+@\w+\.\w+$").AsRegex()) - .Returns(true); - -bool result = sut.ValidateEmail("user@example.com"); - -// Case-sensitive regex -sut.Mock.Setup.Process(It.Matches("^[A-Z]+$").AsRegex().CaseSensitive()) - .Returns(1); -``` - -**Ref and Out Parameters** - -- `It.IsRef(setter)`: Matches any `ref` parameter and sets a new value using the setter function. -- `It.IsRef(predicate, setter)`: Matches `ref` parameters that satisfy the predicate and sets a new value. -- `It.IsRef(predicate)`: Matches `ref` parameters that satisfy the predicate without changing the value. -- `It.IsAnyRef()`: Matches any `ref` parameter without restrictions. -- `It.IsOut(setter)`: Matches any `out` parameter and sets a value using the setter function. -- `It.IsAnyOut()`: Matches any `out` parameter without restrictions and sets the parameter to the default value of - `T`. - -```csharp -// Example: Setup with out parameter -sut.Mock.Setup.TryParse(It.IsAny(), It.IsOut(() => 42)) - .Returns(true); - -int result; -bool success = sut.TryParse("abc", out result); -// result == 42, success == true - -// Example: Setup with ref parameter -sut.Mock.Setup.Increment(It.IsRef(v => v + 1)) - .Returns(true); - -int value = 5; -sut.Increment(ref value); -// value == 6 -``` - -**Collection Matchers** - -- `It.Contains(item)`: Matches a collection parameter that contains `item`. -- `It.SequenceEquals(params IEnumerable values)`: Matches a collection parameter whose elements equal `values` in - the same order. - -Both matchers support method parameters declared as `IEnumerable`, `ICollection`, `IList`, -`IReadOnlyCollection`, `IReadOnlyList`, `T[]`, `List`, `Queue` or `Stack`. -`It.Contains` additionally supports the unordered shapes `ISet` and `HashSet`; `It.SequenceEquals` -intentionally -does not, because their enumeration order is not guaranteed. - -Append `.Using(IEqualityComparer)` to either matcher to control element equality. - -```csharp -// Example: Match a list that contains a specific item -sut.Mock.Setup.Process(It.Contains(5)) - .Returns(true); - -bool result = sut.Process(new[] { 1, 2, 5 }); -// result == true - -// Example: Match a sequence of items in order -sut.Mock.Setup.Process(It.SequenceEquals("a", "b", "c")) - .Returns(true); - -bool match = sut.Process(new[] { "a", "b", "c" }); -// match == true - -// Example: Case-insensitive containment -sut.Mock.Setup.Process(It.Contains("HELLO").Using(StringComparer.OrdinalIgnoreCase)) - .Returns(true); -``` - -**Custom Equality Comparers** - -Use `.Using(IEqualityComparer)` to provide custom equality comparison for `It.Is()` and `It.IsOneOf()`: - -```csharp -// Example: Case-insensitive string comparison -var comparer = StringComparer.OrdinalIgnoreCase; -sut.Mock.Setup.Process(It.Is("hello").Using(comparer)) - .Returns(42); - -int result = sut.Process("HELLO"); -// result == 42 -``` - -**Span Parameters (.NET 8+)** - -- `It.IsSpan(predicate)`: Matches `Span` parameters that satisfy the predicate. -- `It.IsAnySpan()`: Matches any `Span` parameter. -- `It.IsReadOnlySpan(predicate)`: Matches `ReadOnlySpan` parameters that satisfy the predicate. -- `It.IsAnyReadOnlySpan()`: Matches any `ReadOnlySpan` parameter. - -*Note:* -As `ref struct` types cannot be stored directly, it is converted to an array internally and the `predicate` receives -this array for evaluation. - -```csharp -// Example: Setup with Span parameter -sut.Mock.Setup.Process(It.IsSpan(data => data.Length > 0)) - .Returns(true); - -Span buffer = new byte[] { 1, 2, 3 }; -bool result = sut.Process(buffer); -// result == true -``` - -**Ref Struct Parameters (.NET 9+)** - -You can mock methods and indexers that take custom `ref struct` parameters (e.g. `Utf8JsonReader` -or your own `ref struct Packet`) using these matchers: - -- `It.IsAnyRefStruct()`: Matches any ref-struct value of type `T`. -- `It.IsRefStruct(predicate)`: Matches ref-struct values that satisfy the predicate. The - predicate can read the struct's fields at the time the call is made. -- `It.IsRefStructBy(projection)` / `It.IsRefStructBy(projection, predicate)`: - For ref-struct-keyed indexers, projects the key to an equatable value so writes and reads can - be correlated. Works at any arity - apply it to every ref-struct slot and non-ref-struct slots - contribute their raw value to the composite dispatch key (see *Indexer storage* in the remarks). - -```csharp -public readonly ref struct Packet(int id, ReadOnlySpan payload) -{ - public int Id { get; } = id; - public ReadOnlySpan Payload { get; } = payload; -} - -public interface IPacketSink -{ - void Consume(Packet packet); - int TryParse(Packet packet); - string this[Packet key] { get; set; } -} - -// Match on the live ref struct, including its Span contents. -sut.Mock.Setup.Consume(It.IsRefStruct(p => - p.Payload.Length > 0 && p.Payload[0] == 0xFF)) - .Throws(); - -// Return a value from a ref-struct-parameter method. -sut.Mock.Setup.TryParse(It.IsAnyRefStruct()).Returns(42); - -// Ref-struct-keyed indexer: Returns for get, OnSet for observed writes. -sut.Mock.Setup[It.IsAnyRefStruct()] - .Returns("got") - .OnSet(value => { /* observed write */ }); - -// Correlate writes and reads by projecting the key to an equatable value. -sut.Mock.Setup[It.IsRefStructBy(p => p.Id)].Returns("fallback"); -sut[new Packet(1, [])] = "written"; -string a = sut[new Packet(1, [])]; // "written" matched by Id -string b = sut[new Packet(2, [])]; // "fallback" no write under Id=2 -``` - -*Remarks* - -The ref-struct pipeline uses a narrower API than the rest of Mockolate. A handful of fluent -features are unavailable because the C# language does not let `ref struct` values flow through -generic delegates: - -- **Setup surface.** Only `Returns(value)`, `Returns(factory)`, `Throws*`, `OnSet(Action)` - (indexer setters), and `SkippingBaseClass` are available. The `.Do(...)` callback and the - `Callbacks` builder (`InParallel`, `When`, `For`, `Only`, `TransitionTo`) are not offered - for ref-struct parameters. -- **Verify.** `Verify` counts calls to the method but cannot match on the parameter value after - the fact - the ref-struct value isn't retained past the call. Use a setup-time matcher to - filter at call time. -- **Indexer storage.** By default, values written through a ref-struct-keyed indexer setter are - not read back by the getter. Apply `It.IsRefStructBy(projection)` to every ref-struct - slot to enable write-then-read correlation keyed by the projections; non-ref-struct slots - contribute their raw value as part of the composite dispatch key. If any ref-struct slot is - matched without a projection, storage stays inactive for that setup. - -The following cases are rejected at compile time with diagnostic `Mockolate0003`: - -- Targeting older than .NET 9 (the feature relies on `allows ref struct`, a .NET 9 / C# 13 - feature). -- `out` / `ref` / `ref readonly` parameters of a ref-struct type. -- Methods that return a custom ref struct. (`Span` / `ReadOnlySpan` returns are supported.) - -#### Parameter Predicates - -When the method name is unique (no overloads), you can use argument matchers from the `Match` class for more flexible -parameters matching: - -- `Match.AnyParameters()`: Matches any parameter combination. -- `Match.Parameters(Func predicate)`: Matches parameters based on a custom predicate. - -```csharp -// Example: Custom parameter predicate -sut.Mock.Setup.Process(Match.Parameters(args => - args.Length == 2 && - args[0] is string s && s.StartsWith("test") && - args[1] is int i && i > 0)) - .Returns(true); - -bool result = sut.Process("test123", 5); -// result == true -``` - -#### Parameter Interaction - -**Callbacks** - -With `.Do`, you can register a callback for individual parameters of a method setup. This allows you to implement side -effects or checks directly when the method or indexer is called. - -**Example: Do for method parameter** - -```csharp -int lastAmount = 0; -sut.Mock.Setup.Dispense(It.Is("Dark"), It.IsAny().Do(amount => lastAmount = amount)); -sut.Dispense("Dark", 42); -// lastAmount == 42 -``` - -**Monitor** - -With `.Monitor(out monitor)`, you can track the actual -values passed during test execution and analyze them afterward. - -**Example: Monitor for method parameter** - -```csharp -Mockolate.ParameterMonitor monitor; -sut.Mock.Setup.Dispense(It.Is("Dark"), It.IsAny().Monitor(out monitor)); -sut.Dispense("Dark", 5); -sut.Dispense("Dark", 7); -// monitor.Values == [5, 7] -``` - -## Mock events - -Easily raise events on your mock to test event handlers in your code. - -### Raise - -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 -sut.ChocolateDispensed += (type, amount) => { /* handler code */ }; - -// Act: raise the event -sut.Mock.Raise.ChocolateDispensed("Dark", 5); -``` - -- 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 -int dispensedAmount = 0; -sut.ChocolateDispensed += (type, amount) => dispensedAmount += amount; - -sut.Mock.Raise.ChocolateDispensed("Dark", 3); -sut.Mock.Raise.ChocolateDispensed("Milk", 2); - -// dispensedAmount == 5 -``` - -You can subscribe and unsubscribe handlers as needed. Only handlers subscribed at the time of raising the event will be -called. - -## Verify interactions - -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()`: The interaction never occurred -- `.Once()`: The interaction occurred exactly once -- `.Twice()`: The interaction occurred exactly twice -- `.Exactly(n)`: The interaction occurred exactly n times -- `.AtLeastOnce()`: The interaction occurred at least once -- `.AtLeastTwice()`: The interaction occurred at least twice -- `.AtLeast(n)`: The interaction occurred at least n times -- `.AtMostOnce()`: The interaction occurred at most once -- `.AtMostTwice()`: The interaction occurred at most twice -- `.AtMost(n)`: The interaction occurred at most n times -- `.Between(min, max)`: The interaction occurred between min and max times (inclusive) -- `.Times(predicate)`: The interaction count matches the predicate - -If the invocations run in a background thread, you can use `Within(TimeSpan)` to specify a timeout in which to wait for -the expected interactions to occur: - -```csharp -// Wait up to 1 second for Dispense("Dark", 5) to be invoked -sut.Mock.Verify.Dispense(It.Is("Dark"), It.Is(5)) - .Within(TimeSpan.FromSeconds(1)) - .AtLeastOnce(); -``` - -You can also use `WithCancellation(CancellationToken)` to wait for the expected interactions until the cancellation -token is canceled. If you combine this with the `Within` method, both the timeout and the cancellation token are -respected. - -In both cases, it will block the test execution until the expected interaction occurs or the timeout is reached. -If the interaction does not occur within the specified time, a `MockVerificationException` will be thrown. - -If you need truly asynchronous verification without blocking the test thread, you can use the -[aweXpect.Mockolate](https://docs.testably.org/extensions/aweXpect.Mockolate/) NuGet package, which integrates Mockolate's verification -API with [aweXpect](https://docs.testably.org/aweXpect) and offers an awaitable `Within(TimeSpan)` variant. - -### Properties - -You can verify access to property getter and setter: - -```csharp -// Verify that the property 'TotalDispensed' was read at least once -sut.Mock.Verify.TotalDispensed.Got().AtLeastOnce(); - -// Verify that the property 'TotalDispensed' was set to 42 exactly once -sut.Mock.Verify.TotalDispensed.Set(It.Is(42)).Once(); -``` - -**Note:** -The setter value also supports argument matchers. - -### Methods - -You can verify that methods were invoked with specific arguments and how many times: - -```csharp -// Verify that Dispense("Dark", 5) was invoked at least once -sut.Mock.Verify.Dispense(It.Is("Dark"), It.Is(5)) - .AtLeastOnce(); - -// Verify that Dispense was never invoked with "White" and any amount -sut.Mock.Verify.Dispense(It.Is("White"), It.IsAny()) - .Never(); - -// Verify that Dispense was invoked exactly twice with any type and any amount -sut.Mock.Verify.Dispense(Match.AnyParameters()) - .Exactly(2); - -// Verify that Dispense was invoked between 3 and 5 times (inclusive) -sut.Mock.Verify.Dispense(It.IsAny(), It.IsAny()) - .Between(3, 5); - -// Verify that Dispense was invoked an even number of times -sut.Mock.Verify.Dispense(It.IsAny(), It.IsAny()) - .Times(count => count % 2 == 0); -``` - -You can also verify that a specific setup was invoked a specific number of times: - -```csharp -IMockSetup setup = sut.Mock.Setup.Dispense(It.Is("Dark"), It.Is(5)).Returns(true); -// Act -sut.Mock.VerifySetup(setup).AtLeastOnce(); -``` - -### Indexers - -You can verify access to indexer getter and setter: - -```csharp -// Verify that the indexer was read with key "Dark" exactly once -sut.Mock.Verify[It.Is("Dark")].Got().Once(); - -// Verify that the indexer was set with key "Milk" to value 7 at least once -sut.Mock.Verify[It.Is("Milk")].Set(7).AtLeastOnce(); -``` - -**Note:** -The keys and value also supports argument matchers. - -### Events - -You can verify event subscriptions and unsubscriptions: - -```csharp -// Verify that the event 'ChocolateDispensed' was subscribed to at least once -sut.Mock.Verify.ChocolateDispensed.Subscribed().AtLeastOnce(); - -// Verify that the event 'ChocolateDispensed' was unsubscribed from exactly once -sut.Mock.Verify.ChocolateDispensed.Unsubscribed().Once(); -``` - -### Argument Matchers - -You can use argument matchers from the `It` class to verify calls with flexible conditions: - -- `It.IsAny()`: Matches any value of type `T`. -- `It.Is(value)`: Matches a specific value. With `.Using(IEqualityComparer)`, you can provide a custom equality - comparer. -- `It.IsNot(value)`: Matches any value not equal to `value`. -- `It.IsOneOf(params IEnumerable values)`: Matches any of the given values. With `.Using(IEqualityComparer)`, - you can provide a custom equality comparer. -- `It.IsNotOneOf(params IEnumerable values)`: Matches any value that is not in the given set. -- `It.IsNull()`: Matches null. -- `It.IsNotNull()`: Matches any non-null value. -- `It.IsTrue()`/`It.IsFalse()`: Matches boolean true/false. -- `It.IsInRange(min, max)`: Matches a number within the given range. You can append `.Exclusive()` to exclude the - minimum and maximum value. -- `It.IsOut()`: Matches any out parameter of type `T`. -- `It.IsRef()`: Matches any ref parameter of type `T`. -- `It.IsSpan(predicate)` / `It.IsAnySpan()`: Matches `Span` parameters (.NET 8+). -- `It.IsReadOnlySpan(predicate)` / `It.IsAnyReadOnlySpan()`: Matches `ReadOnlySpan` parameters (.NET 8+). -- `It.Matches(pattern)`: Matches strings using wildcard patterns (`*` and `?`). With `.AsRegex()`, you can use - regular expressions instead. -- `It.Contains(item)`: Matches a collection parameter that contains `item`. With `.Using(IEqualityComparer)`, you - can provide a custom equality comparer. -- `It.SequenceEquals(params IEnumerable values)`: Matches a collection parameter whose elements equal `values` in - order. With `.Using(IEqualityComparer)`, you can provide a custom equality comparer. -- `It.Satisfies(predicate)`: Matches values based on a predicate. - -*Note:* Custom `ref struct` matchers (`It.IsRefStruct`, `It.IsAnyRefStruct`, `It.IsRefStructBy`) only -apply at setup time - `Verify` counts calls to ref-struct members but cannot match on the value after the fact, since -the ref-struct value isn't retained past the call. - -**Example:** - -```csharp -sut.Mock.Verify.Dispense(It.Is(t => t.StartsWith("D")), It.IsAny()).Once(); -sut.Mock.Verify.Dispense(It.Is("Milk"), It.IsAny()).AtLeastOnce(); -``` - -### Call Ordering - -Use `Then` to verify that calls occurred in a specific order: - -```csharp -sut.Mock.Verify.Dispense(It.Is("Dark"), It.Is(2)).Then( - m => m.Dispense(It.Is("Dark"), It.Is(3)) -); -``` - -You can chain multiple calls for strict order verification: - -```csharp -sut.Mock.Verify.Dispense(It.Is("Dark"), It.Is(1)).Then( - m => m.Dispense(It.Is("Milk"), It.Is(2)), - m => m.Dispense(It.Is("White"), It.Is(3))); -``` - -If the order is incorrect or a call is missing, a `MockVerificationException` will be thrown with a descriptive message. - -## Advanced Features - -### Working with protected members - -Mockolate allows you to set up and verify protected virtual members on class mocks. - -Protected members can be set up, raised, and verified just like instance members, but through the `Mock.SetupProtected`, -`Mock.RaiseProtected`, and `Mock.VerifyProtected` properties: - -**Example** - -```csharp -public abstract class ChocolateDispenser -{ - protected virtual bool DispenseInternal(string type, int amount) => true; - protected virtual int InternalStock { get; set; } -} - -ChocolateDispenser sut = ChocolateDispenser.CreateMock(); -``` - -#### Setup - -```csharp -// Setup protected method -sut.Mock.SetupProtected.DispenseInternal( - It.Is("Dark"), It.IsAny()) - .Returns(true); - -// Setup protected property -sut.Mock.SetupProtected.InternalStock.InitializeWith(100); -``` - -**Notes:** - -- Protected members can be set up and verified just like public members. -- All setup options (`.Returns()`, `.Throws()`, `.Do()`, `.InitializeWith()`, etc.) work with protected members. - -#### Verification - -```csharp -// Verify protected method was invoked -sut.Mock.VerifyProtected.DispenseInternal( - It.Is("Dark"), It.IsAny()).Once(); - -// Verify protected property was read -sut.Mock.VerifyProtected.InternalStock.Got().AtLeastOnce(); - -// Verify protected property was set -sut.Mock.VerifyProtected.InternalStock.Set(It.Is(100)).Once(); - -// Verify protected indexer was read -sut.Mock.VerifyProtected[It.Is(0)].Got().Once(); - -// Verify protected indexer was set -sut.Mock.VerifyProtected[It.Is(0)].Set(It.Is(42)).Once(); -``` - -**Note:** - -- All verification options (argument matchers, count assertions) work the same for protected members as for public - members. - -### Advanced callback features - -#### Conditional callbacks (`When`) - -Execute callbacks conditionally based on the zero-based invocation counter using `.When()`: - -```csharp -sut.Mock.Setup.Dispense(It.Is("Dark"), It.IsAny()) - .Do(() => Console.WriteLine("Called!")).When(count => count >= 2); // The first two calls are skipped -``` - -#### Limit invocations (`Only`) - -Control after how many times a callback should no longer be executed: - -```csharp -// Execute up to 3 times -sut.Mock.Setup.Dispense(It.IsAny(), It.IsAny()) - .Do(() => Console.WriteLine("Up to 3 times")).Only(3); - -// Executes the callback only once -sut.Mock.Setup.TotalDispensed - .Throws(new Exception("This exception is thrown only once")).OnlyOnce(); -``` - -#### Repeat invocations (`For`) - -Control how many times a callback should be repeated: - -```csharp -sut.Mock.Setup.Dispense(It.IsAny(), It.IsAny()) - .Do(() => Console.WriteLine("First three times")).For(3) - .Do(() => Console.WriteLine("Next three times")).For(3); - -sut.Mock.Setup.TotalDispensed - .Returns(10).For(1) - .Returns(20).For(2) - .Returns(30).For(3); -// Reads: 10, 20, 20, 30, 30, 30, 0, 0, 0, 0 … -``` - -**Repeat `Forever`** - -If you have a sequence of callbacks, you can mark the last one to repeat indefinitely using `.Forever()` to avoid -repeating the sequence from start: - -```csharp -sut.Mock.Setup.Dispense(It.IsAny(), It.IsAny()) - .Returns(true).For(2) // Returns true the first two times - .Returns(false).Forever(); // Then always returns false -``` - -#### Parallel callbacks - -When you specify multiple callbacks, they are executed sequentially by default. You can change this behavior to always -run specific callbacks in parallel using `.InParallel()`: - -```csharp -sut.Mock.Setup.Dispense(It.IsAny(), It.IsAny()) - .Do(() => { Console.WriteLine("Runs every second iteration"); }) - .Do(() => { Console.WriteLine("Runs always in parallel"); }).InParallel() - .Do(() => { Console.WriteLine("Runs every other iteration"); }); -``` - -**Note:** -Parallel execution via `.InParallel()` only applies to callbacks defined via `Do`, not to other setup callbacks like -`Returns` or `Throws`. - -#### Invocation counter - -Access the zero-based invocation counter in callbacks: - -```csharp -sut.Mock.Setup.Dispense(It.IsAny(), It.IsAny()) - .Do((count, _, _) => Console.WriteLine($"Call #{count}")); - -sut.Mock.Setup.TotalDispensed.OnGet - .Do((count, value) => Console.WriteLine($"Read #{count}, value: {value}")); - -// Indexer setter - count, then the indexer key(s), then the new value -sut.Mock.Setup[It.IsAny()].OnSet - .Do((count, type, newValue) => - Console.WriteLine($"Set #{count}: this[{type}] = {newValue}")); -``` - -### Monitor interactions - -Mockolate tracks all interactions with mocks on the mock object. To only track interactions within a given scope, you -can use a `MockMonitor`: - -```csharp -var sut = IChocolateDispenser.CreateMock(); -var monitor = sut.Mock.Monitor(); - -sut.Dispense("Dark", 1); // Not monitored -using (monitor.Run()) -{ - sut.Dispense("Dark", 2); // Monitored -} -sut.Dispense("Dark", 3); // Not monitored - -// Verifications on the monitor only count interactions during the lifetime scope of the `IDisposable` -monitor.Verify.Dispense(It.Is("Dark"), It.IsAny()).Once(); -``` - -#### Clear all interactions - -For simpler scenarios you can directly clear all recorded interactions on a mock using `ClearAllInteractions` on the -setup: - -```csharp -IChocolateDispenser sut = IChocolateDispenser.CreateMock(); - -sut.Dispense("Dark", 1); -// Clears all previously recorded interactions -sut.Mock.ClearAllInteractions(); -sut.Dispense("Dark", 2); - -sut.Mock.Verify.Dispense(It.Is("Dark"), It.IsAny()).Once(); -``` - -### Check for unexpected interactions - -#### That all interactions are verified - -You can check if all interactions with the mock have been verified using `VerifyThatAllInteractionsAreVerified`: - -```csharp -// Returns true if all interactions have been verified before -bool allVerified = sut.Mock.VerifyThatAllInteractionsAreVerified(); -``` - -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`. - -#### That all setups are used - -You can check if all registered setups on the mock have been used with `VerifyThatAllSetupsAreUsed`: - -```csharp -// Returns true if all setups have been used -bool allUsed = sut.Mock.VerifyThatAllSetupsAreUsed(); -``` - -This is useful for ensuring that your test setup and test execution match. -If any setup was not used, this method returns `false`. - -### Static interface members (.NET 8+) - -Mockolate supports mocking static abstract and static virtual members on interfaces (.NET 8+). Static member -invocations use async-flow scoping, meaning each mock instance has its own isolated static member context, -this makes parallel test execution safe. - -Static members can be set up, raised, and verified just like instance members, but through the `Mock.SetupStatic`, -`Mock.RaiseStatic`, and `Mock.VerifyStatic` properties: - -```csharp -// Setup static members -sut.Mock.SetupStatic.AbstractStaticMethod().Returns("some-value"); -sut.Mock.SetupStatic.AbstractStaticProperty.Returns("some-value"); - -// Raise static events -sut.Mock.RaiseStatic.AbstractStaticEvent(value); - -// Verify static interactions -sut.Mock.VerifyStatic.AbstractStaticMethod().Once(); -sut.Mock.VerifyStatic.AbstractStaticProperty.Got().Once(); -sut.Mock.VerifyStatic.AbstractStaticEvent.Subscribed().Once(); -``` - -**Notes:** - -- Static member scoping is implemented via `AsyncLocal`. When you call - `sut.Mock.SetupStatic.Method()`, it creates an async-flow scope that routes static member invocations to that - specific mock instance. -- Each mock instance has an independent static member context, so parallel tests will not interfere with each other. - -### Scenarios - -Scenarios let you define multiple sets of setups on a single mock and switch between them at runtime. This is useful -when the collaborator behaves differently depending on its internal state — for example a connection that starts -disconnected, becomes connected after `Connect()`, and times out after a failure. - -A mock always has an *active scenario* (the empty string `""` by default). When a member is accessed, Mockolate looks -for a matching setup in the active scenario first, then falls back to the default scope. - -#### Defining scenarios - -Use `InScenario(name)` to scope setups to a named scenario. It returns a scope whose `Setup` property mirrors -`sut.Mock.Setup` but targets the scenario's bucket: - -```csharp -sut.Mock.InScenario("disconnected").Setup.Ping().Throws(); -sut.Mock.InScenario("connected").Setup.Ping().Returns(true); -``` - -A callback overload batches multiple setups into the same scenario: - -```csharp -sut.Mock.InScenario("connected", scope => -{ - scope.Setup.Ping().Returns(true); - scope.Setup.Send(It.IsAny()).Returns(true); -}); -``` - -Setups registered via `InScenario` do **not** leak into the default scope. - -#### Switching scenarios - -Chain `.TransitionTo(name)` on any method, property, indexer, or event subscription/unsubscription setup to change -the active scenario when the setup fires. The transition runs as a parallel side-effect — it does not replace -the return value or throw behaviour. - -```csharp -sut.Mock.InScenario("disconnected") - .Setup.Connect() - .Returns(true) - .TransitionTo("connected"); - -sut.Mock.InScenario("connected") - .Setup.Ping() - .Throws() - .TransitionTo("disconnected"); -``` - -You can also change the active scenario manually via `sut.Mock.TransitionTo("connected");`, which is useful for -arranging the starting state. - -**Resolution rules** - -When dispatching a call, Mockolate looks up setups in this order: - -1. The active scenario's bucket (if non-empty). -2. The default bucket (setups registered via `sut.Mock.Setup.*`). -3. The mock's default behaviour. - -Scenario setups add to, rather than replace, the default scope — register catch-alls in the default scope and -override specific members per scenario. - -## Special Types - -### HttpClient - -Mockolate supports mocking `HttpClient` out of the box, with no special configuration required. You can set up, use, and -verify HTTP interactions just like with any other interface or class. - -**Example: Mocking HttpClient for a Chocolate Dispenser Service** - -```csharp -HttpClient httpClient = HttpClient.CreateMock(); -httpClient.Mock.Setup - .PostAsync( - It.IsAny(), - It.IsHttpContent()) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); - -HttpResponseMessage result = await httpClient.PostAsync("https://testably.org/api/chocolate/dispense", - new StringContent(""" - { "type": "Dark", "amount": 3 } - """, Encoding.UTF8, "application/json")); - -await That(result.IsSuccessStatusCode).IsTrue(); -httpClient.Mock.Verify.PostAsync( - It.IsUri("*testably.org/api/chocolate/dispense*").ForHttps(), - It.IsHttpContent("application/json").WithStringMatching("*\"type\": \"Dark\"*\"amount\": 3*")).Once(); -``` - -**Notes:** - -- The custom extensions for the `HttpClient` are in the `Mockolate.Web` namespace. -- Under the hood, the setups, requests and verifications are forwarded to a mocked `HttpMessageHandler`. - As they therefore all forward to the `SendAsync` method, you can mix using a string or an `Uri` parameter in setup or - verification. - -#### All HTTP Methods - -Mockolate supports all standard HTTP methods: - -```csharp -// GET -httpClient.Mock.Setup - .GetAsync(It.IsAny()) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.NoContent)); - -// POST -httpClient.Mock.Setup - .PostAsync(It.IsAny(), It.IsAny()) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); - -// PUT -httpClient.Mock.Setup - .PutAsync(It.IsAny(), It.IsAny()) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); - -// DELETE -httpClient.Mock.Setup - .DeleteAsync(It.IsAny()) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.NoContent)); - -// PATCH -httpClient.Mock.Setup - .PatchAsync(It.IsAny(), It.IsAny()) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); -``` - -For all HTTP methods you can add an optional cancellation token parameter. -If no parameter is provided, it matches any `CancellationToken`: - -```csharp -var cts = new CancellationTokenSource(); -httpClient.Mock.Setup - .GetAsync(It.IsAny(), It.Is(cts.Token)) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); - -await httpClient.GetAsync("https://example.com", cts.Token); -``` - -#### URI matching - -Use `It.IsUri(string?)` to match URIs using a wildcard pattern against the string representation of the URI. -The pattern supports `*` to match zero or more characters and `?` to match a single character. - -**Scheme** - -Filter requests by URI scheme using `.ForHttps()` or `.ForHttp()`: - -```csharp -// Match only HTTPS requests -httpClient.Mock.Verify - .GetAsync(It.IsUri("*testably.org*").ForHttps()) - .Once(); - -// Match only HTTP requests -httpClient.Mock.Verify - .GetAsync(It.IsUri("*testably.org*").ForHttp()) - .Never(); -``` - -**Host** - -Filter requests by host using `.WithHost(string)`. You can provide a wildcard pattern to match against the host name: - -```csharp -httpClient.Mock.Verify - .GetAsync(It.IsUri().WithHost("*testably.org*")) - .Once(); -``` - -**Port** - -Filter requests on a specific port using `.WithPort(int)`: - -```csharp -httpClient.Mock.Verify - .GetAsync(It.IsUri().WithPort(443)) - .Once(); -``` - -**Path** - -Filter requests by path using `.WithPath(string)`. You can provide a wildcard pattern to match against the path: - -```csharp -httpClient.Mock.Verify - .GetAsync(It.IsUri().WithPath("/foo/*")) - .Once(); -``` - -**Query** - -Filter requests by query parameters using `.WithQuery(...)`. You can provide one or many key-value pairs or a raw query -string to match against the query parameters. The order of the key-value pairs does not matter: - -```csharp -// Match query string containing "x=42" -httpClient.Mock.Verify - .GetAsync(It.IsUri().WithQuery("x", "42")) - .Once(); -// Match query string containing "x=42" and "y=foo" (in any order) -httpClient.Mock.Verify - .GetAsync(It.IsUri().WithQuery(("x", "42"), ("y", "foo"))) - .Once(); -// Match query string containing "x=42" and "y=foo" (in any order) -httpClient.Mock.Verify - .GetAsync(It.IsUri().WithQuery("x=42&y=foo")) - .Once(); -``` - -#### Content Matching - -Use `It.IsHttpContent(string?)` to match the HTTP content, optionally providing an expected media type header value: - -```csharp -httpClient.Mock.Setup - .PostAsync( - It.IsAny(), - It.IsHttpContent("application/json")) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); -``` - -**String content** - -To verify against the string content, use the following methods: - -- `.WithString(Func)`: to match the string content against the given predicate -- `.WithString(string)`: to match the content exactly as provided -- `.WithStringMatching(string)`: to match the content using wildcard patterns -- `.WithStringMatching(string).AsRegex()`: to match the content using regular expressions - -```csharp -httpClient.Mock.Setup - .PostAsync( - It.IsAny(), - It.IsHttpContent("application/json").WithStringMatching("*\"type\": \"Dark\"*")) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); -``` - -*Notes:* - -- Add the `.IgnoringCase()` modifier to make the string matching case-insensitive. - -**Binary content** - -To verify against the binary content, use the following methods: - -- `.WithBytes(Func)`: to match the binary content against the given predicate -- `.WithBytes(byte[])`: to match the content exactly as provided - -```csharp -httpClient.Mock.Setup - .PostAsync( - It.IsAny(), - It.IsHttpContent("application/octet-stream").WithBytes([0x01, 0x02, 0x03, ])) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); -``` - -**Form data content** - -To verify against the URL-encoded form data content, use the following methods: - -- `.WithFormData(string, string)`: checks that the form-data content contains the provided key-value pair -- `.WithFormData(IEnumerable<(string, string)>)`: checks that the form-data content contains the provided key-value - pairs -- `.WithFormData(string)`: checks that the form-data content contains the provided raw form data string - -```csharp -httpClient.Mock.Setup - .PostAsync( - It.IsAny(), - It.IsHttpContent("application/x-www-form-urlencoded").WithFormData("my-key", "my-value")) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); -``` - -*Notes:* - -- Similar to the query parameter matching, the order of the form-data key-value pairs does not matter. -- Add the `.Exactly()` modifier to also check that no other form-data is present. - -**Header matching** - -To verify against the HTTP content headers, use the following methods: - -- `.WithHeaders(string, HttpHeaderValue)`: checks that the content headers contain the provided key-value pair -- `.WithHeaders(IEnumerable<(string, HttpHeaderValue)>)`: checks that the content headers contain all key-value pairs -- `.WithHeaders(string)`: checks that the content headers contain the provided raw headers - -```csharp -httpClient.Mock.Setup - .PostAsync( - It.IsAny(), - It.IsHttpContent().WithHeaders(("Content-Type", "application/json"), ("X-My-Header", "my-value"))) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); -httpClient.Mock.Setup - .PostAsync( - It.IsAny(), - It.IsHttpContent().WithHeaders(""" - Content-Type: application/json - X-My-Header: my-value - """)) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); -``` - -*Notes:* - -- By default, only the content headers are checked, not the headers in the corresponding `HttpRequestMessage`. - If you want to check both, add the `.IncludingRequestHeaders()` modifier. - -#### Return Message - -Overloads of `.ReturnsAsync` simplify specifying the return value for HTTP method setups. - -```csharp -httpClient.Mock.Setup.GetAsync(It.IsAny()) - // Returns a response with status code 200 OK and no content - .ReturnsAsync(HttpStatusCode.OK); - -httpClient.Mock.Setup.GetAsync(It.IsAny()) - // Returns a response with status code 200 OK and a string content "some string content" - .ReturnsAsync(HttpStatusCode.OK, "some string content"); - -httpClient.Mock.Setup.GetAsync(It.IsAny()) - // Returns a response with status code 200 OK and a JSON content {"foo":"bar"} - .ReturnsAsync(HttpStatusCode.OK, "{\"foo\":\"bar\"}", "application/json"); - -byte[] bytes = new byte[] { /* ... */ }; - -httpClient.Mock.Setup.GetAsync(It.IsAny()) - // Returns a response with status code 200 OK and a binary content with the provided bytes - .ReturnsAsync(HttpStatusCode.OK, bytes); - -httpClient.Mock.Setup.GetAsync(It.IsAny()) - // Returns a response with status code 200 OK and a PNG image content with the provided bytes - .ReturnsAsync(HttpStatusCode.OK, bytes, "image/png"); -``` - -#### Whole-request matching - -Use `It.IsHttpRequestMessage(HttpMethod?)` together with `Setup.SendAsync(...)` to match against the entire -`HttpRequestMessage`, optionally restricted to a specific HTTP method. This is the catch-all entry point that the -verb-specific overloads (`GetAsync`, `PostAsync`, …) route through internally - reach for it when you need the full -request (e.g. to inspect headers, or to handle non-standard verbs like `HEAD` or `OPTIONS`). - -Chain builders to add constraints: - -- `.WhoseUriIs(string)` / `.WhoseUriIs(Action)`: same surface as `It.IsUri(...)`. -- `.WhoseContentIs(Action)` / `.WhoseContentIs(string mediaType, …)`: same surface as - `It.IsHttpContent(...)`. -- `.WithHeaders(...)`: require specific request headers. - -```csharp -// Match any POST to /api/chocolate/dispense with a JSON body containing "Dark" -httpClient.Mock.Setup - .SendAsync(It.IsHttpRequestMessage(HttpMethod.Post) - .WhoseUriIs(uri => uri.WithPath("/api/chocolate/dispense")) - .WhoseContentIs("application/json", c => c.WithStringMatching("*\"type\": \"Dark\"*"))) - .ReturnsAsync(HttpStatusCode.OK); +```powershell +dotnet add package Mockolate ``` -### Delegates - -Mockolate supports mocking delegates including `Action`, `Func`, and custom delegates. - -**Setup** - -Use `sut.Mock.Setup(…)` to configure delegate behavior. - -```csharp -// Mock Action delegate -Action myAction = Action.CreateMock(); -myAction.Mock.Setup().Do(() => Console.WriteLine("Action invoked!")); - -// Mock Func delegate -Func myFunc = Func.CreateMock(); -myFunc.Mock.Setup().Returns(42); -``` - -For custom delegates with parameters: - -```csharp -// Define a custom delegate (typically declared at type level) -public delegate int Calculate(int x, string operation); - -// Create and setup the mock -Calculate calculator = Calculate.CreateMock(); -calculator.Mock.Setup(It.IsAny(), It.Is("add")) - .Returns((x, operation) => x + 10); -``` - -Delegates with `ref` and `out` parameters are also supported: - -```csharp -// Define a custom delegate (typically declared at type level) -public delegate void ProcessData(int input, ref int value, out int result); - -// Create and setup the mock -ProcessData processor = ProcessData.CreateMock(); -processor.Mock.Setup(It.IsAny(), It.IsRef(v => v + 1), It.IsOut(() => 100)); -``` - -- Use `.Do(…)` to run code when the delegate is invoked. -- Use `.Returns(…)` to specify the return value for `Func` delegates. -- Use `.Throws(…)` to specify an exception to throw. -- Use `.Returns(…)` and `.Throws(…)` repeatedly to define a sequence of behaviors. -- Full [parameter matching](#parameter-matching) support for delegate - parameters including `ref` and `out` parameters. - -**Verification** - -You can verify that delegates were invoked with specific arguments: - -```csharp -// Verify Action was invoked at least once -Action myAction = Action.CreateMock(); -myAction.Invoke(); -myAction.Mock.Verify().AtLeastOnce(); - -// Verify Func was invoked exactly once -Func myFunc = Func.CreateMock(); -_ = myFunc(); -myFunc.Mock.Verify().Once(); -``` - -For custom delegates with parameters: - -```csharp -// Define a custom delegate (typically declared at type level) -public delegate int Calculate(int x, string operation); - -// Create, invoke, and verify the mock -Calculate calculator = Calculate.CreateMock(); -_ = calculator(5, "add"); -calculator.Mock.Verify(It.IsAny(), It.Is("add")).Once(); -``` - -Delegates with `ref` and `out` parameters are also supported: - -```csharp -// Define a custom delegate (typically declared at type level) -public delegate void ProcessData(int input, ref int value, out int result); - -// Create, invoke, and verify the mock -ProcessData processor = ProcessData.CreateMock(); -int val = 0; -processor(1, ref val, out int res); -processor.Mock.Verify(It.IsAny(), It.IsRef(), It.IsOut()).Once(); -``` - -**Note:** -Delegate parameters also support [argument matchers](#argument-matchers). - -## A complete example - -The following example combines properties, indexers, events, methods, stateful setup, and verification -into a single end-to-end scenario: +## Quick Start ```csharp using Mockolate; -public delegate void ChocolateDispensedDelegate(string type, int amount); public interface IChocolateDispenser { - int this[string type] { get; set; } - int TotalDispensed { get; set; } bool Dispense(string type, int amount); - event ChocolateDispensedDelegate ChocolateDispensed; } -// Create a mock of IChocolateDispenser +// Create a mock IChocolateDispenser sut = IChocolateDispenser.CreateMock(); -// Setup: Initial stock of 10 for Dark chocolate -sut.Mock.Setup["Dark"].InitializeWith(10); -// Setup: Dispense decreases Dark chocolate if enough, returns true/false -sut.Mock.Setup.Dispense("Dark", It.IsAny()) - .Returns((type, amount) => - { - int current = sut[type]; - if (current >= amount) - { - sut[type] = current - amount; - sut.Mock.Raise.ChocolateDispensed(type, amount); - return true; - } - return false; - }); - -// Track dispensed amount via event -int dispensedAmount = 0; -sut.ChocolateDispensed += (type, amount) => -{ - dispensedAmount += amount; -}; - -// Act: Try to dispense chocolates -bool gotChoc1 = sut.Dispense("Dark", 4); // true -bool gotChoc2 = sut.Dispense("Dark", 5); // true -bool gotChoc3 = sut.Dispense("Dark", 6); // false - -// Verify: Check interactions -sut.Mock.Verify.Dispense("Dark", It.IsAny()).Exactly(3); -``` - -## Analyzers - -Mockolate ships with some Roslyn analyzers to help you adopt best practices and catch issues early, at compile time. -All rules provide actionable messages and link to identifiers for easy filtering. - -### Mockolate0001 - -`Verify` methods only return a `VerificationResult` and do not directly throw. You have to specify how often you expect -the call to happen, e.g. `.AtLeastOnce()`, `.Exactly(n)`, etc. or use the verification result in any other way. - -**Example:** - -```csharp -IChocolateDispenser sut = IChocolateDispenser.CreateMock(); -sut.Dispense("Dark", 1); -// Analyzer Mockolate0001: Add a count assertion like .AtLeastOnce() or use the result. -sut.Mock.Verify.Dispense(It.Is("Dark"), It.IsAny()); -``` +// Setup: Dispense returns true for any Dark chocolate request +sut.Mock.Setup.Dispense("Dark", It.IsAny()).Returns(true); -The included code fixer suggests to add the `.AtLeastOnce()` count assertion: +// Act +bool success = sut.Dispense("Dark", 4); -```csharp -sut.Mock.Verify.Dispense(It.Is("Dark"), It.IsAny()).AtLeastOnce(); +// Verify +sut.Mock.Verify.Dispense("Dark", It.IsAny()).Once(); ``` -### Mockolate0002 - -Mocked types must be mockable. This rule will prevent you from using unsupported types: - -- `CreateMock()` - Type must be an interface, a delegate or a supported class (e.g. not sealed) -- `Implementing()` - Type must be an interface - -### Mockolate0003 - -A mocked member's signature routes through the ref-struct pipeline in a way Mockolate can't -emit setup surface for. The warning fires in two distinct situations. - -**1. Compilation prerequisites not met** - -The ref-struct setup pipeline requires both: - -- A target framework of .NET 9 or later (Mockolate's ref-struct setup types are - `#if NET9_0_OR_GREATER`-gated). -- An effective C# language version of 13 or later (uses the `allows ref struct` anti-constraint). - -When either prerequisite is missing, the warning fires for any member that passes a non-`Span` / -non-`ReadOnlySpan` ref struct by value, or uses one as an indexer key. Upgrade the target -framework and/or `` to resolve it. - -**2. Signature shapes that are never supported** - -These fire on every compilation target, including .NET 9+ / C# 13+: - -- Parameters marked `out`, `ref`, or `ref readonly` whose type is a non-`Span` / - non-`ReadOnlySpan` ref struct - the mock can't round-trip the value through - `IOutParameter` / `IRefParameter` when `T` is a ref struct. -- Methods returning a non-`Span` / non-`ReadOnlySpan` ref struct. +## Documentation -**Note:** -`Span` and `ReadOnlySpan` flow through the existing `SpanWrapper` / `ReadOnlySpanWrapper` -fallback and are never flagged. On .NET 9+ with C# 13+, by-value custom ref-struct parameters and -ref-struct-keyed indexers (getter-only, setter-only, and get+set) are fully supported. +Full reference docs at **[docs.testably.org/Mockolate](https://docs.testably.org/Mockolate/)**: -See the Ref Struct Parameters section for the supported surface. +- [Create mocks](https://docs.testably.org/Mockolate/create-mocks) +- Setup: [properties](https://docs.testably.org/Mockolate/setup/properties), [methods](https://docs.testably.org/Mockolate/setup/methods), [indexers](https://docs.testably.org/Mockolate/setup/indexers), [parameter matching](https://docs.testably.org/Mockolate/setup/parameter-matching) +- [Mock events](https://docs.testably.org/Mockolate/mock-events) +- [Verify interactions](https://docs.testably.org/Mockolate/verify-interactions) +- Advanced features: [protected members](https://docs.testably.org/Mockolate/advanced-features/protected-members), [static interface members](https://docs.testably.org/Mockolate/advanced-features/static-interface-members), [callbacks](https://docs.testably.org/Mockolate/advanced-features/advanced-callback-features), [monitors](https://docs.testably.org/Mockolate/advanced-features/monitor-interactions), [scenarios](https://docs.testably.org/Mockolate/advanced-features/scenarios), [unexpected-interaction checks](https://docs.testably.org/Mockolate/advanced-features/check-for-unexpected-interactions) +- Special types: [`HttpClient`](https://docs.testably.org/Mockolate/special-types/httpclient), [delegates](https://docs.testably.org/Mockolate/special-types/delegates)