Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
96 changes: 96 additions & 0 deletions src/Mimic/Arg.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,61 +4,157 @@

namespace Mimic;

/// <summary>
/// Provides argument matchers for use in mock setups and verifications.
/// </summary>
[PublicAPI]
public static class Arg
{
/// <summary>
/// Matches any value of type <typeparamref name="TValue"/>, including null.
/// </summary>
/// <typeparam name="TValue">The type of the argument to match.</typeparam>
/// <returns>An argument matcher that matches any value of the specified type.</returns>
public static TValue Any<TValue>() => typeof(TValue).IsOrContainsGenericMatcher()
? ArgumentMatcher.Create<TValue>((argument, parameterType) => argument == null || parameterType.IsInstanceOfType(argument))!
: ArgumentMatcher.Create<TValue>(argument => argument == null || argument is TValue)!;

/// <summary>
/// Matches any non-null value of type <typeparamref name="TValue"/>.
/// </summary>
/// <typeparam name="TValue">The type of the argument to match.</typeparam>
/// <returns>An argument matcher that matches any non-null value of the specified type.</returns>
public static TValue AnyNotNull<TValue>() => typeof(TValue).IsOrContainsGenericMatcher()
? ArgumentMatcher.Create<TValue>((argument, parameterType) => argument != null && parameterType.IsInstanceOfType(argument))!
: ArgumentMatcher.Create<TValue>(argument => argument is TValue)!;

/// <summary>
/// Matches an argument that is equal to the specified value using the default equality comparer.
/// </summary>
/// <typeparam name="TValue">The type of the argument to match.</typeparam>
/// <param name="value">The value to match against.</param>
/// <returns>An argument matcher that matches the specified value.</returns>
/// <remarks>
/// This overload does not support generic matchers due to the strongly typed value.
/// For generic matchers, use the overload with <c>Expression&lt;Func&lt;object, Type, bool&gt;&gt; match</c> instead.
/// </remarks>
public static TValue Is<TValue>(TValue value)
{
Guard.Assert(!typeof(TValue).IsOrContainsGenericMatcher(), "This overload does not support generic matchers due to the strongly typed value. Please use the overload with `Expression<Func<object, Type, bool>> predicate` instead.");

return ArgumentMatcher.Create<TValue>(argument => Equals(argument, value))!;
}

/// <summary>
/// Matches an argument that is equal to the specified value using the provided equality comparer.
/// </summary>
/// <typeparam name="TValue">The type of the argument to match.</typeparam>
/// <param name="value">The value to match against.</param>
/// <param name="comparer">The equality comparer to use for comparison.</param>
/// <returns>An argument matcher that matches the specified value using the provided comparer.</returns>
/// <remarks>
/// This overload does not support generic matchers due to the strongly typed value.
/// For generic matchers, use the overload with <c>Expression&lt;Func&lt;object, Type, bool&gt;&gt; match</c> instead.
/// </remarks>
public static TValue Is<TValue>(TValue value, IEqualityComparer<TValue> comparer)
{
Guard.Assert(!typeof(TValue).IsOrContainsGenericMatcher(), "This overload does not support generic matchers due to the strongly typed value. Please use the overload with `Expression<Func<object, Type, bool>> predicate` instead.");

return ArgumentMatcher.Create<TValue>(argument => comparer.Equals(argument, value))!;
}

/// <summary>
/// Matches an argument that satisfies the specified predicate expression.
/// </summary>
/// <typeparam name="TValue">The type of the argument to match.</typeparam>
/// <param name="match">The predicate expression that the argument must satisfy.</param>
/// <returns>An argument matcher that matches arguments satisfying the predicate.</returns>
/// <remarks>
/// This overload does not support generic matchers due to the strongly typed value.
/// For generic matchers, use the overload with <c>Expression&lt;Func&lt;object, Type, bool&gt;&gt; match</c> instead.
/// </remarks>
public static TValue Is<TValue>(Expression<Func<TValue, bool>> match)
{
Guard.Assert(!typeof(TValue).IsOrContainsGenericMatcher(), "This overload does not support generic matchers due to the strongly typed value. Please use the overload with `Expression<Func<object, Type, bool>> predicate` instead.");

return ArgumentMatcher.Create<TValue>(argument => match.Compile().Invoke(argument!))!;
}

/// <summary>
/// Matches an argument that satisfies the specified predicate expression with type information.
/// This overload supports generic matchers.
/// </summary>
/// <typeparam name="TValue">The type of the argument to match.</typeparam>
/// <param name="match">The predicate expression that takes the argument and its parameter type.</param>
/// <returns>An argument matcher that matches arguments satisfying the predicate.</returns>
public static TValue Is<TValue>(Expression<Func<object, Type, bool>> match) =>
ArgumentMatcher.Create<TValue>((argument, parameterType) => match.Compile().Invoke(argument!, parameterType))!;

/// <summary>
/// Matches an argument contained in the specified collection of values.
/// </summary>
/// <typeparam name="TValue">The type of the argument to match.</typeparam>
/// <param name="values">The collection of values to check against.</param>
/// <returns>An argument matcher that matches values contained in the collection.</returns>
public static TValue In<TValue>(IEnumerable<TValue> values) =>
ArgumentMatcher.Create<TValue>(argument => values.Contains(argument))!;

/// <summary>
/// Matches an argument contained in the specified collection of values using the provided equality comparer.
/// </summary>
/// <typeparam name="TValue">The type of the argument to match.</typeparam>
/// <param name="values">The collection of values to check against.</param>
/// <param name="comparer">The equality comparer to use for comparison.</param>
/// <returns>An argument matcher that matches values contained in the collection using the comparer.</returns>
public static TValue In<TValue>(IEnumerable<TValue> values, IEqualityComparer<TValue> comparer) =>
ArgumentMatcher.Create<TValue>(argument => values.Contains(argument, comparer!))!;

/// <summary>
/// Matches an argument contained in the specified array of values.
/// </summary>
/// <typeparam name="TValue">The type of the argument to match.</typeparam>
/// <param name="values">The array of values to check against.</param>
/// <returns>An argument matcher that matches values contained in the array.</returns>
public static TValue In<TValue>(params TValue[] values) =>
ArgumentMatcher.Create<TValue>(argument => values.Contains(argument))!;

/// <summary>
/// Matches an argument not contained in the specified collection of values.
/// </summary>
/// <typeparam name="TValue">The type of the argument to match.</typeparam>
/// <param name="values">The collection of values to check against.</param>
/// <returns>An argument matcher that matches values not contained in the collection.</returns>
public static TValue NotIn<TValue>(IEnumerable<TValue> values) =>
ArgumentMatcher.Create<TValue>(argument => !values.Contains(argument))!;

/// <summary>
/// Matches an argument not contained in the specified collection of values using the provided equality comparer.
/// </summary>
/// <typeparam name="TValue">The type of the argument to match.</typeparam>
/// <param name="values">The collection of values to check against.</param>
/// <param name="comparer">The equality comparer to use for comparison.</param>
/// <returns>An argument matcher that matches values not contained in the collection using the comparer.</returns>
public static TValue NotIn<TValue>(IEnumerable<TValue> values, IEqualityComparer<TValue> comparer) =>
ArgumentMatcher.Create<TValue>(argument => !values.Contains(argument, comparer!))!;

/// <summary>
/// Matches an argument not contained in the specified array of values.
/// </summary>
/// <typeparam name="TValue">The type of the argument to match.</typeparam>
/// <param name="values">The array of values to check against.</param>
/// <returns>An argument matcher that matches values not contained in the array.</returns>
public static TValue NotIn<TValue>(params TValue[] values) =>
ArgumentMatcher.Create<TValue>(argument => !values.Contains(argument))!;

/// <summary>
/// Provides reference argument matchers for ref parameters.
/// </summary>
/// <typeparam name="TValue">The type of the reference argument.</typeparam>
public static class Ref<TValue>
{
/// <summary>
/// Matches any reference argument of type <typeparamref name="TValue"/>.
/// </summary>
#pragma warning disable CA2211
public static TValue Any = default!;
#pragma warning restore CA2211
Expand Down
56 changes: 56 additions & 0 deletions src/Mimic/CallCount.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
namespace Mimic;

/// <summary>
/// Represents a call count constraint that can be used to verify the number of times a method was invoked.
/// </summary>
[PublicAPI]
public readonly struct CallCount : IEquatable<CallCount>
{
Expand All @@ -14,28 +17,62 @@ private CallCount(Type type, int from, int to)
_to = to;
}

/// <summary>
/// Creates a call count constraint that requires at least one invocation.
/// </summary>
/// <returns>A <see cref="CallCount"/> that validates if a method was called at least once.</returns>
public static CallCount AtLeastOnce() => new(Type.AtLeastOnce, 1, int.MaxValue);

/// <summary>
/// Creates a call count constraint that requires at least the specified number of invocations.
/// </summary>
/// <param name="count">The minimum number of invocations required.</param>
/// <returns>A <see cref="CallCount"/> that validates if a method was called at least the specified number of times.</returns>
/// <exception cref="Guard.AssertionException">Thrown when <paramref name="count"/> is less than 1.</exception>
public static CallCount AtLeast(int count)
{
Guard.Assert(count >= 1);
return new CallCount(Type.AtLeast, count, int.MaxValue);
}

/// <summary>
/// Creates a call count constraint that allows at most the specified number of invocations.
/// </summary>
/// <param name="count">The maximum number of invocations allowed.</param>
/// <returns>A <see cref="CallCount"/> that validates if a method was called at most the specified number of times.</returns>
/// <exception cref="Guard.AssertionException">Thrown when <paramref name="count"/> is less than 1.</exception>
public static CallCount AtMost(int count)
{
Guard.Assert(count >= 1);
return new CallCount(Type.AtMost, 0, count);
}

/// <summary>
/// Creates a call count constraint that allows at most one invocation.
/// </summary>
/// <returns>A <see cref="CallCount"/> that validates if a method was called at most once.</returns>
public static CallCount AtMostOnce() => new(Type.AtMostOnce, 0, 1);

/// <summary>
/// Creates a call count constraint that requires the number of invocations to be within the specified range (inclusive).
/// </summary>
/// <param name="from">The minimum number of invocations (inclusive).</param>
/// <param name="to">The maximum number of invocations (inclusive).</param>
/// <returns>A <see cref="CallCount"/> that validates if a method was called within the specified range.</returns>
/// <exception cref="Guard.AssertionException">Thrown when <paramref name="from"/> is negative or <paramref name="to"/> is less than <paramref name="from"/>.</exception>
public static CallCount InclusiveBetween(int from, int to)
{
Guard.Assert(from >= 0 && to >= from);
return new CallCount(Type.InclusiveBetween, from, to);
}

/// <summary>
/// Creates a call count constraint that requires the number of invocations to be within the specified range (exclusive).
/// </summary>
/// <param name="from">The minimum number of invocations (exclusive).</param>
/// <param name="to">The maximum number of invocations (exclusive).</param>
/// <returns>A <see cref="CallCount"/> that validates if a method was called within the specified range.</returns>
/// <exception cref="Guard.AssertionException">Thrown when <paramref name="from"/> is not positive, <paramref name="to"/> is not greater than <paramref name="from"/>, or the difference between <paramref name="to"/> and <paramref name="from"/> is 1.</exception>
public static CallCount ExclusiveBetween(int from, int to)
{
Guard.Assert(from > 0 && to > from);
Expand All @@ -44,16 +81,35 @@ public static CallCount ExclusiveBetween(int from, int to)
return new CallCount(Type.ExclusiveBetween, from + 1, to - 1);
}

/// <summary>
/// Creates a call count constraint that requires exactly the specified number of invocations.
/// </summary>
/// <param name="count">The exact number of invocations required.</param>
/// <returns>A <see cref="CallCount"/> that validates if a method was called exactly the specified number of times.</returns>
/// <exception cref="Guard.AssertionException">Thrown when <paramref name="count"/> is not positive.</exception>
public static CallCount Exactly(int count)
{
Guard.Assert(count > 0);
return new CallCount(Type.Exactly, count, count);
}

/// <summary>
/// Creates a call count constraint that requires exactly one invocation.
/// </summary>
/// <returns>A <see cref="CallCount"/> that validates if a method was called exactly once.</returns>
public static CallCount Once() => new(Type.Once, 1, 1);

/// <summary>
/// Creates a call count constraint that requires zero invocations.
/// </summary>
/// <returns>A <see cref="CallCount"/> that validates if a method was never called.</returns>
public static CallCount Never() => new(Type.Never, 0, 0);

/// <summary>
/// Validates whether the specified call count satisfies this constraint.
/// </summary>
/// <param name="count">The actual number of invocations to validate.</param>
/// <returns><c>true</c> if the call count satisfies this constraint; otherwise, <c>false</c>.</returns>
public bool Validate(int count) => _from <= count && count <= _to;

public bool Equals(CallCount other) => _from == other._from && _to == other._to;
Expand Down
Loading
Loading