Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
809155e
feat(mocks): add ClearInvocations, CallBase toggle and string/type/re…
claude Oct 10, 2026
6a84e33
Merge pull request #3 from Quattordici/claude/mocks-clear-invocations…
Quattordici Oct 10, 2026
e9c2819
fix(mocks): don't auto-track indexer setters with CallBase=false; doc…
Quattordici Oct 10, 2026
da3aa67
refactor(mocks): share CallBase=false handling, strict beats DefaultV…
Quattordici Oct 10, 2026
cc647e5
docs(mocks): IsOfType doc describes setup-time ArgumentException
Quattordici Oct 10, 2026
4ddf8d1
docs(mocks): fix orphaned TryHandleCall docs; document ClearInvocatio…
Quattordici Oct 10, 2026
eb41313
refactor(mocks): rename CallBase to PassThrough and ClearInvocations …
Quattordici Oct 10, 2026
3e11df2
test(mocks): cover Contains overload, wrap+strict+PassThrough; cleare…
Quattordici Oct 10, 2026
25ecad2
docs(mocks): note net8 ClearCalls form; broaden IsOfType error guidance
Quattordici Oct 10, 2026
76c57ba
fix(mocks): address PassThrough/ClearCalls review findings
Quattordici Oct 10, 2026
157b493
Merge branch 'main' into main
Quattordici Oct 10, 2026
391db11
fix(mocks): scope PassThrough=false base-call and provider handling
Quattordici Oct 10, 2026
7818c18
fix(mocks): don't swallow type mismatches in static-abstract return c…
Quattordici Oct 10, 2026
5e23bad
docs/test(mocks): clarify ClearCalls and Reset semantics, pin edge cases
Quattordici Oct 10, 2026
3323a4e
refactor(mocks): opt-in interfaces for ClearCalls/PassThrough; Reset …
Quattordici Oct 10, 2026
1bc95cb
docs(mocks): note that IsOfType/IsSameAs sentinels need a typed conve…
Quattordici Oct 10, 2026
9b4bb3d
docs/test(mocks): fix doubled period, rename pass-through heading, pi…
Quattordici Oct 10, 2026
3f9fb36
test(mocks): pin that ClearCalls never exposes a half-reset call history
Quattordici Oct 10, 2026
60f83c2
test(mocks): make the ClearCalls race test exercise the per-member co…
Quattordici Oct 11, 2026
de1b02f
Merge branch 'main' into main
Quattordici Oct 11, 2026
6dba86e
docs/test(mocks): document ClearCalls race and Moq sequence note; pin…
Quattordici Oct 11, 2026
1ed1b40
Merge upstream main into fork main
Quattordici Oct 11, 2026
cb57636
docs/test(mocks): comment wrap-strict precedence; pin single-arg sett…
Quattordici Oct 11, 2026
ed4291c
fix(mocks): address review on matchers, ResetInvokeCount and PassThro…
thomhurst Oct 11, 2026
a7d7b48
fix(mocks): IsSameAs matches equal values for value types; dedupe unm…
thomhurst Oct 11, 2026
bb67916
fix(mocks): identify object-method overrides by symbol; align IsSameA…
thomhurst Oct 11, 2026
aff3117
fix(mocks): IsOfType compares nullable parameters by their underlying…
thomhurst Oct 11, 2026
05d3db4
fix(mocks): pass auto-mock factory through indexer getters; honor arr…
thomhurst Oct 11, 2026
e8ecf43
fix(mocks): discover auto-mock factories for interfaces returned by i…
thomhurst Oct 11, 2026
c415775
fix(mocks): limit IsOfType array covariance to interfaces arrays impl…
thomhurst Oct 11, 2026
8b83e1f
refactor(mocks): route untyped unmatched calls through shared helpers…
thomhurst Oct 11, 2026
d5cff53
fix(mocks): object-method bases pass through nested calls; skip inacc…
thomhurst Oct 11, 2026
9a4803a
refactor(mocks): StringMatcher names its Arg factory explicitly in de…
thomhurst Oct 11, 2026
9887b66
fix(mocks): object methods read auto-tracked property values when Pas…
thomhurst Oct 11, 2026
b657e9a
docs(mocks): note ClearCalls is not atomic against concurrent verific…
thomhurst Oct 11, 2026
d103ae5
docs(mocks): Mock.ClearCalls documents invoke-count reset and non-ato…
thomhurst Oct 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/docs/migration/mocking/fakeiteasy.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ TUnit.Mocks works with any test framework. You can migrate mocks before, after,
| `A.CallToSet(() => fake.Prop).To(value)` | `mock.Prop.Set(value)` |
| `fake.Event += Raise.With(args)` | `mock.Raise{EventName}(args)` |
| `Fake.GetCalls(fake)` | `mock.Invocations` |
| `Fake.ClearRecordedCalls(fake)` | `mock.Reset()` (also clears setups) |
| `Fake.ClearRecordedCalls(fake)` | `mock.ClearCalls()` (keeps setups) |

<!-- doc-test-shared -->

Expand Down Expand Up @@ -440,7 +440,7 @@ calculator.Discount(Any()).Returns(5m);
decimal tax = calculator.Object.Tax(100m); // base implementation: 20
```

If a test depends on FakeItEasy's default behavior, configure each virtual member that the test calls. TUnit.Mocks can also configure `protected` virtual and abstract members with the same syntax as public members.
If a test depends on FakeItEasy's default behavior, set `mock.PassThrough = false`; unconfigured virtual members then return default values instead of running the base implementation. TUnit.Mocks can also configure `protected` virtual and abstract members with the same syntax as public members.

## Other Features

Expand All @@ -451,7 +451,7 @@ If a test depends on FakeItEasy's default behavior, configure each virtual membe
| `A.Fake<Func<int, string>>()` | `Mock.OfDelegate<Func<int, string>>()` |
| `A.CallTo(fake).Where(call => ...)` | No equivalent; configure each member |
| `Fake.GetCalls(fake)` | `mock.Invocations` (a list of `CallRecord`) |
| `Fake.ClearRecordedCalls(fake)` | `mock.Reset()`; it also clears setups and state |
| `Fake.ClearRecordedCalls(fake)` | `mock.ClearCalls()`; it keeps setups and state |

See [Advanced Features](../../writing-tests/mocking/advanced.md) for state machines, diagnostics, custom default values, and `MockRepository`.

Expand Down
5 changes: 3 additions & 2 deletions docs/docs/migration/mocking/moq.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ TUnit.Mocks works with any test framework. You can migrate mocks before, after,
| `mock.As<IOther>()` | `Mock.Of<IService, IOther>()` |
| `Mock.Get(instance)` | `Mock.Get(instance)` |
| `mock.Invocations` | `mock.Invocations` |
| `mock.Invocations.Clear()` | `mock.ClearCalls()` |
| `mock.Reset()` | `mock.Reset()` |
| `new MockRepository(MockBehavior.Strict)` | `new MockRepository(MockBehavior.Strict)` |

Expand Down Expand Up @@ -457,7 +458,7 @@ calculator.Discount(Any()).Returns(5m);
decimal tax = calculator.Object.Tax(100m); // base implementation: 20
```

If a test depends on Moq's default `CallBase = false`, configure each virtual member that the test calls.
If a test depends on Moq's default `CallBase = false`, set `calculator.PassThrough = false`. Unconfigured virtual members then return default values (or throw in strict mode) instead of running the base implementation.

Moq configures `protected` members with `mock.Protected().Setup<T>("Name", ...)` and string names. TUnit.Mocks generates typed members for `protected` virtual and abstract members, so you configure and verify them like public members.

Expand All @@ -470,7 +471,7 @@ Moq configures `protected` members with `mock.Protected().Setup<T>("Name", ...)`
| `mock.As<IDisposable>()` | `Mock.Of<IUserRepository, IDisposable>()` (up to four types) |
| `Mock.Of<IService>(x => x.Prop == value)` | `IService.Mock()` and then `mock.Prop.Returns(value)` |
| `new Mock<Func<int, string>>()` | `Mock.OfDelegate<Func<int, string>>()` |
| `mock.Invocations.Clear()` | `mock.Reset()` (also clears setups and state) |
| `mock.Invocations.Clear()` | `mock.ClearCalls()` (keeps setups and state) |
Comment thread
Quattordici marked this conversation as resolved.
Outdated
| `Mock.Get(instance)` | `Mock.Get(instance)` |

To wrap an existing instance of a non-sealed class and override only some of its virtual members, use `Mock.Wrap(instance)`. `Mock.Wrap` does not support interfaces. See [Advanced Features](../../writing-tests/mocking/advanced.md) for state machines, diagnostics, and `MockRepository`.
Expand Down
6 changes: 3 additions & 3 deletions docs/docs/migration/mocking/nsubstitute.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ TUnit.Mocks works with any test framework. You can migrate mocks before, after,
| `sub.ReceivedWithAnyArgs().Method(default)` | `mock.Method(Any()).WasCalled()` (`Any()` for each parameter; see [Ignoring Arguments](#ignoring-arguments)) |
| `Received.InOrder(() => { ... })` | `Mock.VerifyInOrder(() => { ... })` |
| `sub.ReceivedCalls()` | `mock.Invocations` |
| `sub.ClearReceivedCalls()` | `mock.Reset()` (also clears setups) |
| `sub.ClearReceivedCalls()` | `mock.ClearCalls()` (keeps setups) |
| `sub.Event += Raise.EventWith(args)` | `mock.Raise{EventName}(args)` |
| Auto-properties on substitutes | `mock.SetupAllProperties()` |

Expand Down Expand Up @@ -443,7 +443,7 @@ calculator.Discount(Any()).Returns(5m);
decimal tax = calculator.Object.Tax(100m); // base implementation: 20
```

This is different from `Substitute.For<SomeClass>()`, which does not call the base implementation of virtual members. If you depend on that, configure each member you call. Pass constructor arguments as typed parameters: `MyService.Mock("connection", 42)`.
This is different from `Substitute.For<SomeClass>()`, which does not call the base implementation of virtual members. If you depend on that, set `mock.PassThrough = false`; unconfigured virtual members then return default values instead of running the base implementation. Pass constructor arguments as typed parameters: `MyService.Mock("connection", 42)`.

TUnit.Mocks can also configure `protected` virtual and abstract members with the same syntax as public members. To wrap an existing instance of a non-sealed class and override only some of its virtual members, use `Mock.Wrap(instance)`. `Mock.Wrap` does not support interfaces.

Expand All @@ -453,7 +453,7 @@ TUnit.Mocks can also configure `protected` virtual and abstract members with the
|---|---|
| Recursive mocks (interface members return substitutes) | Loose mocks return auto-mocks; use `Mock.Get(instance)` to configure one |
| `sub.ReceivedCalls()` | `mock.Invocations` (a list of `CallRecord`) |
| `sub.ClearReceivedCalls()` | `mock.Reset()`; it also clears setups and state |
| `sub.ClearReceivedCalls()` | `mock.ClearCalls()`; it keeps setups and state |
| `Substitute.For<IA, IB>()` | `Mock.Of<IA, IB>()` (up to four types) |
| `.Configure()` before setting up a partial | Not needed; setup never calls the real member |

Expand Down
40 changes: 40 additions & 0 deletions docs/docs/writing-tests/mocking/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,46 @@ _ = mock.Invocations.Count; // 0 (history cleared)

The `SetupAllProperties()` flag is preserved across resets.

## Clearing Recorded Calls

`Reset()` removes everything. To forget only the recorded calls and keep your setups, use `ClearCalls()`. This suits a test that acts twice and verifies only the second act:

```csharp
mock.GetUser(Any()).Returns(new User("Alice"));
svc.GetUser(1);

mock.ClearCalls();
Comment thread
coderabbitai[bot] marked this conversation as resolved.

_ = mock.Invocations.Count; // 0 (history cleared)
svc.GetUser(1); // setup kept: still returns Alice
mock.GetUser(1).WasCalled(Times.Once); // counts only the call after the clear
```

Setups, state, auto-tracked property values and event subscriptions are kept. `VerifyAll()` still sees which setups were invoked. `MockRepository.ClearCalls()` clears every tracked mock. The instance-style `mock.ClearCalls()` is available on net9 and later; on net8 and older target frameworks use `Mock.ClearCalls(mock)`.

## Call Base on Class Mocks

By default a class mock runs the base implementation of any virtual member you have not configured. Set `PassThrough` to `false` to make unconfigured members behave like interface members instead: loose mocks return smart defaults (and auto-mocks for interface return types), and strict mocks throw `MockStrictBehaviorException`.

<!-- doc-test-declaration: split-before=// Usage -->
```csharp
public class ShippingCalculator
{
public virtual decimal Tax(decimal amount) => amount * 0.2m;
public virtual decimal Discount(decimal amount) => amount * 0.1m;
}

// Usage
var calculator = ShippingCalculator.Mock();
calculator.PassThrough = false;
calculator.Discount(Any()).Returns(5m);

_ = calculator.Object.Discount(100m); // 5 (configured)
_ = calculator.Object.Tax(100m); // 0 (base implementation is not called)
```

Calls are still recorded and can be verified. `PassThrough` can be changed at any time. It has no effect on interface or abstract members because they have no base implementation. On a `Mock.Wrap(...)` mock, `false` stops unconfigured calls from reaching the wrapped instance. On net8 and older target frameworks, use `Mock.SetPassThrough(mock, false)` and `Mock.GetPassThrough(mock)`.

## Internals Access (experimental)

Some SDKs route behavior through types that are `internal` to their own assembly — the classic
Expand Down
44 changes: 44 additions & 0 deletions docs/docs/writing-tests/mocking/argument-matchers.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ TUnit.Mocks automatically imports the `Arg` class via `global using static`, so
| `IsNotNull<T>()` | Any non-null value |
| `Matches(pattern)` | String matching a regex pattern |
| `Matches(regex)` | String matching a compiled `Regex` |
| `StartsWith(prefix)` | String starting with a prefix (optional `StringComparison`) |
| `EndsWith(suffix)` | String ending with a suffix (optional `StringComparison`) |
| `Contains(substring)` | String containing a substring (optional `StringComparison`) |
| `Contains<TCol, TElem>(item)` | Collection containing an element |
| `IsOfType<TDerived>()` | Instance of `TDerived` (or a subtype), for a base-typed parameter |
| `IsSameAs(reference)` | The exact same instance (reference equality) |
| `HasCount<T>(n)` | Collection with exactly n elements |
| `IsEmpty<T>()` | Empty collection |
| `SequenceEquals<TCol, TElem>(expected)` | Collection matching element-by-element |
Expand Down Expand Up @@ -162,6 +167,45 @@ var pattern = new Regex(@"^user_\d+$", RegexOptions.Compiled);
mock.Search(Matches(pattern)).Returns(new[] { "found" });
```

### StartsWith, EndsWith and Contains

```csharp
mock.Greet(StartsWith("Dr. ")).Returns("Hello, doctor");
mock.Greet(EndsWith("Jr.")).Returns("Hello, junior");
mock.Greet(Contains("smith", StringComparison.OrdinalIgnoreCase)).Returns("Hello, Smith");
```

Matching is case-sensitive (`StringComparison.Ordinal`) unless you pass a comparison. `null` never matches. `Contains("text")` with a string is a different overload from the collection form `Contains<TCollection, TElement>(item)`.

## Type and Reference Matchers

`IsOfType<TDerived>()` matches an argument that is an instance of `TDerived` or a subtype. The parameter type is inferred, so it lets you constrain a base-typed parameter. `IsSameAs(instance)` matches only that exact instance, ignoring any `Equals` override.

<!-- doc-test-declaration: split-before=// Usage -->
```csharp
public class AuditEvent { }
public class LoginEvent : AuditEvent { }
public class LogoutEvent : AuditEvent { }

public interface IAuditSink
{
string Record(AuditEvent evt);
}

// Usage
var sink = IAuditSink.Mock();
var logout = new LogoutEvent();

sink.Record(IsOfType<LoginEvent>()).Returns("login");
sink.Record(IsSameAs<AuditEvent>(logout)).Returns("that logout");

_ = sink.Object.Record(new LoginEvent()); // "login"
_ = sink.Object.Record(logout); // "that logout"
_ = sink.Object.Record(new LogoutEvent()); // "" (no match)
```

`null` never matches `IsOfType`.

## Collection Matchers

### Contains
Expand Down
16 changes: 14 additions & 2 deletions src/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1401,6 +1401,7 @@ private static void GenerateIndexerExtensionMethods(CodeWriter writer, MockMembe
// matching Mock static helper).
private static readonly (string Name, string ReturnType, string Parameters, string ForwardArgs)[] MethodPolyfills =
{
("ClearCalls", "void", "", ""),
("Reset", "void", "", ""),
("VerifyAll", "void", "", ""),
("VerifyNoOtherCalls", "void", "", ""),
Expand All @@ -1421,11 +1422,12 @@ private static void GenerateMockControlPolyfills(CodeWriter writer, MockTypeMode
var emitInvocations = !userNames.Contains("Invocations");
var emitBehavior = !userNames.Contains("Behavior");
var emitDefaultValueProvider = !userNames.Contains("DefaultValueProvider");
var emitPassThrough = !userNames.Contains("PassThrough");
Comment thread
thomhurst marked this conversation as resolved.
Outdated

// Skip entirely when every polyfill name collides with a user member — keeps the
// separator blank line (below) and the surrounding #if/#endif from being emitted.
if (emitMethods.Length == 0 && !emitInState
&& !emitInvocations && !emitBehavior && !emitDefaultValueProvider)
&& !emitInvocations && !emitBehavior && !emitDefaultValueProvider && !emitPassThrough)
{
return;
}
Expand Down Expand Up @@ -1466,7 +1468,7 @@ private static void GenerateMockControlPolyfills(CodeWriter writer, MockTypeMode
// Roslyn 4.14) reject the attribute on non-indexer extension properties with CS9262
// (#6370), and it ranks nothing anyway — the skip-emission above already guarantees no
// same-name member ever coexists in this containing type.
if (emitInvocations || emitBehavior || emitDefaultValueProvider)
if (emitInvocations || emitBehavior || emitDefaultValueProvider || emitPassThrough)
{
if (!first) writer.AppendLine();
using (writer.Block($"extension{typeParams}(global::TUnit.Mocks.Mock<{mockableType}> mock){constraints}"))
Expand All @@ -1491,6 +1493,16 @@ private static void GenerateMockControlPolyfills(CodeWriter writer, MockTypeMode
writer.AppendLine("get => global::TUnit.Mocks.Mock.GetDefaultValueProvider(mock);");
writer.AppendLine("set => global::TUnit.Mocks.Mock.SetDefaultValueProvider(mock, value);");
}
firstProp = false;
}
if (emitPassThrough)
{
if (!firstProp) writer.AppendLine();
using (writer.Block("public bool PassThrough"))
{
writer.AppendLine("get => global::TUnit.Mocks.Mock.GetPassThrough(mock);");
writer.AppendLine("set => global::TUnit.Mocks.Mock.SetPassThrough(mock, value);");
}
}
}
}
Expand Down
35 changes: 35 additions & 0 deletions src/TUnit.Mocks/Arguments/Arg.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,29 @@ public static class Arg
/// <summary>Matches a string against a compiled <see cref="Regex"/>.</summary>
public static Arg<string> Matches(Regex regex) => new(new RegexMatcher(regex));

/// <summary>Matches a string that starts with <paramref name="prefix"/>. Null never matches.</summary>
public static Arg<string> StartsWith(string prefix, StringComparison comparison = StringComparison.Ordinal)
=> new(new StringMatcher(StringMatcher.Mode.StartsWith, prefix, comparison));

/// <summary>Matches a string that ends with <paramref name="suffix"/>. Null never matches.</summary>
public static Arg<string> EndsWith(string suffix, StringComparison comparison = StringComparison.Ordinal)
=> new(new StringMatcher(StringMatcher.Mode.EndsWith, suffix, comparison));

/// <summary>Matches a string that contains <paramref name="substring"/>. Null never matches.</summary>
public static Arg<string> Contains(string substring, StringComparison comparison = StringComparison.Ordinal)
=> new(new StringMatcher(StringMatcher.Mode.Contains, substring, comparison));

/// <summary>
/// Matches an argument that is an instance of <typeparamref name="TDerived"/> (or a type derived from it).
/// The type is inferred from the parameter position, so a base-typed parameter can be constrained to a
/// subtype. Null never matches. If <typeparamref name="TDerived"/> can never overlap the parameter type
/// (not checked at compile time), an <see cref="ArgumentException"/> is thrown when the matcher is set up.
/// </summary>
public static OfTypeArg IsOfType<TDerived>() => new(typeof(TDerived));

/// <summary>Matches only the same object instance as <paramref name="reference"/> (reference equality).</summary>
public static Arg<T> IsSameAs<T>(T reference) where T : class => new(new ReferenceEqualsMatcher<T>(reference));
Comment thread
Quattordici marked this conversation as resolved.
Outdated

/// <summary>Matches using a user-defined custom matcher.</summary>
public static Arg<T> Matches<T>(IArgumentMatcher<T> matcher) => new(matcher);

Expand Down Expand Up @@ -83,3 +106,15 @@ public sealed class AnyArg
internal static readonly AnyArg Instance = new();
private AnyArg() { }
}

/// <summary>
/// Sentinel type returned by <see cref="Arg.IsOfType{TDerived}"/> that implicitly converts to
/// <see cref="Arg{T}"/> for any T, so the parameter type is inferred from context.
/// </summary>
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public sealed class OfTypeArg
{
internal Type Type { get; }

internal OfTypeArg(Type type) => Type = type;
}
3 changes: 3 additions & 0 deletions src/TUnit.Mocks/Arguments/ArgOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,7 @@ public Arg(IArgumentMatcher matcher)
public static implicit operator Arg<T>(Func<T, bool> predicate) => new(new PredicateMatcher<T>(predicate!));

public static implicit operator Arg<T>(AnyArg _) => new(AnyMatcher<T>.Instance);

/// <summary>Converts <see cref="Arg.IsOfType{TDerived}"/> into a type matcher for this parameter type.</summary>
public static implicit operator Arg<T>(OfTypeArg ofType) => new(new TypeMatcher<T>(ofType.Type));
}
3 changes: 3 additions & 0 deletions src/TUnit.Mocks/IMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ public interface IMock
/// <summary>Fails if any recorded call was not matched by a prior verification.</summary>
void VerifyNoOtherCalls();

/// <summary>Clears recorded call history only, keeping setups and state.</summary>
void ClearCalls();

/// <summary>Clears all setups and call history.</summary>
void Reset();
}
6 changes: 6 additions & 0 deletions src/TUnit.Mocks/IMockControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ internal interface IMockControl<T> : IMock where T : class
/// </summary>
IDefaultValueProvider? DefaultValueProvider { get; set; }

/// <summary>
/// Whether unconfigured virtual members of a class or wrap mock run the base implementation (default true).
/// When false they return default values (loose) or throw (strict), like interface members.
/// </summary>
bool PassThrough { get; set; }

/// <summary>
/// Enables auto-tracking for all properties. Setters store, getters return.
/// Explicit setups take precedence.
Expand Down
19 changes: 19 additions & 0 deletions src/TUnit.Mocks/Matchers/ReferenceEqualsMatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using TUnit.Mocks.Arguments;

namespace TUnit.Mocks.Matchers;

/// <summary>
/// Matches only the exact same object instance, ignoring any <c>Equals</c> override.
/// </summary>
internal sealed class ReferenceEqualsMatcher<T> : IArgumentMatcher<T> where T : class
{
private readonly T _expected;

public ReferenceEqualsMatcher(T expected) => _expected = expected ?? throw new ArgumentNullException(nameof(expected));

public bool Matches(T? value) => ReferenceEquals(_expected, value);

public bool Matches(object? value) => ReferenceEquals(_expected, value);

public string Describe() => "Arg.IsSameAs(" + _expected.GetType().Name + " instance)";
}
48 changes: 48 additions & 0 deletions src/TUnit.Mocks/Matchers/StringMatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using TUnit.Mocks.Arguments;

namespace TUnit.Mocks.Matchers;

/// <summary>
/// Matches non-null string arguments by prefix, suffix or substring.
/// </summary>
internal sealed class StringMatcher : IArgumentMatcher<string>
{
internal enum Mode
{
StartsWith,
EndsWith,
Contains,
}

private readonly string _text;
private readonly Mode _mode;
private readonly StringComparison _comparison;

public StringMatcher(Mode mode, string text, StringComparison comparison)
{
_text = text ?? throw new ArgumentNullException(nameof(text));
Comment thread
thomhurst marked this conversation as resolved.
Outdated
_mode = mode;
_comparison = comparison;
}

public bool Matches(string? value)
{
if (value is null)
{
return false;
}

return _mode switch
{
Mode.StartsWith => value.StartsWith(_text, _comparison),
Mode.EndsWith => value.EndsWith(_text, _comparison),
_ => value.IndexOf(_text, _comparison) >= 0,
};
}

public bool Matches(object? value) => value is string s && Matches(s);

public string Describe() => _comparison == StringComparison.Ordinal
? $"Arg.{_mode}(\"{_text}\")"
: $"Arg.{_mode}(\"{_text}\", {_comparison})";
}
Loading
Loading