Skip to content

Commit 66991e4

Browse files
committed
Improve output for expected argument matchers
- Add IDescribeSpecification to allow custom arg matchers to provide custom output for "expected to receive" entries. - Fallback to ToString when IDescribeSpecification not implemented. - Update code comment docs accordingly. - Improved support for custom matchers Closes #796.
1 parent 9567b02 commit 66991e4

File tree

9 files changed

+156
-9
lines changed

9 files changed

+156
-9
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,3 +308,7 @@ docs/_site/*
308308

309309
# Ignore Ionide files (https://ionide.io/)
310310
.ionide
311+
312+
# Ignore mergetool temp files
313+
*.orig
314+

src/NSubstitute/Arg.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,18 @@ public static ref T Is<T>(Expression<Predicate<object?>> predicate) where T : An
5353
return ref ArgumentMatcher.Enqueue<T>(new ExpressionArgumentMatcher<object>(predicate));
5454
}
5555

56+
/// <summary>
57+
/// Match argument that satisfies <paramref name="matcher"/>.
58+
/// </summary>
59+
public static ref T Is<T>(IArgumentMatcher matcher) =>
60+
ref ArgumentMatcher.Enqueue<T>(matcher);
61+
62+
/// <summary>
63+
/// Match argument that satisfies <paramref name="matcher"/>.
64+
/// </summary>
65+
public static ref T Is<T>(IArgumentMatcher<T> matcher) =>
66+
ref ArgumentMatcher.Enqueue(matcher);
67+
5668
/// <summary>
5769
/// Invoke any <see cref="Action"/> argument whenever a matching call is made to the substitute.
5870
/// </summary>

src/NSubstitute/ArgMatchers.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using NSubstitute.Core.Arguments;
2+
3+
// Disable nullability for client API, so it does not affect clients.
4+
#nullable disable annotations
5+
6+
namespace NSubstitute;
7+
8+
/// <summary>
9+
/// Argument matchers for use with <see cref="Arg.Is{T}(IArgumentMatcher{T})"/>.
10+
/// </summary>
11+
public static class ArgMatchers
12+
{
13+
public static IArgumentMatcher<T> EqualTo<T>(T value) => new TypedEqualsArgumentMatcher<T>(value);
14+
15+
public static IArgumentMatcher Any<T>() => new AnyArgumentMatcher(typeof(T));
16+
17+
18+
#if NET6_0_OR_GREATER
19+
/// <summary>
20+
/// Match argument that satisfies <paramref name="predicate"/>.
21+
/// If the <paramref name="predicate"/> throws an exception for an argument it will be treated as non-matching.
22+
/// </summary>
23+
public static IArgumentMatcher<T> Matching<T>(
24+
Predicate<T> predicate,
25+
[System.Runtime.CompilerServices.CallerArgumentExpression("predicate")]
26+
string predicateDescription = ""
27+
) =>
28+
new PredicateArgumentMatcher<T>(predicate, predicateDescription);
29+
30+
// See https://github.com/nsubstitute/NSubstitute/issues/822
31+
private class PredicateArgumentMatcher<T>(Predicate<T> predicate, string predicateDescription) : IArgumentMatcher<T>
32+
{
33+
public bool IsSatisfiedBy(T argument) => predicate(argument!);
34+
35+
public override string ToString() => predicateDescription;
36+
}
37+
#endif
38+
}

src/NSubstitute/Compatibility/Arg.Compat.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Linq.Expressions;
2+
using NSubstitute.Core.Arguments;
23

34
// Disable nullability for client API, so it does not affect clients.
45
#nullable disable annotations
@@ -48,6 +49,16 @@ public static class Compat
4849
/// </summary>
4950
public static AnyType Is<T>(Expression<Predicate<object>> predicate) where T : AnyType => Arg.Is<T>(predicate);
5051

52+
/// <summary>
53+
/// Match argument that satisfies <paramref name="matcher"/>.
54+
/// </summary>
55+
public static T Is<T>(IArgumentMatcher matcher) => ArgumentMatcher.Enqueue<T>(matcher);
56+
57+
/// <summary>
58+
/// Match argument that satisfies <paramref name="matcher"/>.
59+
/// </summary>
60+
public static T Is<T>(IArgumentMatcher<T> matcher) => ArgumentMatcher.Enqueue(matcher);
61+
5162
/// <summary>
5263
/// Invoke any <see cref="Action"/> argument whenever a matching call is made to the substitute.
5364
/// This is provided for compatibility with older compilers --
@@ -95,7 +106,7 @@ public static class Compat
95106
/// Capture any argument compatible with type <typeparamref name="T"/> and use it to call the <paramref name="useArgument"/> function
96107
/// whenever a matching call is made to the substitute.
97108
/// This is provided for compatibility with older compilers --
98-
/// if possible use <see cref="Arg.Do{T}" /> instead.
109+
/// if possible use <see cref="Arg.Do{T}(System.Action{T})" /> instead.
99110
/// </summary>
100111
public static T Do<T>(Action<T> useArgument) => Arg.Do<T>(useArgument);
101112

src/NSubstitute/Compatibility/CompatArg.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Linq.Expressions;
2+
using NSubstitute.Core.Arguments;
23

34
// Disable nullability for client API, so it does not affect clients.
45
#nullable disable annotations
@@ -57,6 +58,16 @@ private CompatArg() { }
5758
/// </summary>
5859
public Arg.AnyType Is<T>(Expression<Predicate<object>> predicate) where T : Arg.AnyType => Arg.Is<T>(predicate);
5960

61+
/// <summary>
62+
/// Match argument that satisfies <paramref name="matcher"/>.
63+
/// </summary>
64+
public static T Is<T>(IArgumentMatcher matcher) => ArgumentMatcher.Enqueue<T>(matcher);
65+
66+
/// <summary>
67+
/// Match argument that satisfies <paramref name="matcher"/>.
68+
/// </summary>
69+
public static T Is<T>(IArgumentMatcher<T> matcher) => ArgumentMatcher.Enqueue(matcher);
70+
6071
/// <summary>
6172
/// Invoke any <see cref="Action"/> argument whenever a matching call is made to the substitute.
6273
/// This is provided for compatibility with older compilers --
Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
namespace NSubstitute.Core.Arguments;
22

3-
public class AnyArgumentMatcher(Type typeArgMustBeCompatibleWith) : IArgumentMatcher
3+
public class AnyArgumentMatcher(Type typeArgMustBeCompatibleWith)
4+
: IArgumentMatcher, IDescribeSpecification, IDescribeNonMatches
45
{
56
public override string ToString() => "any " + typeArgMustBeCompatibleWith.GetNonMangledTypeName();
67

78
public bool IsSatisfiedBy(object? argument) => argument.IsCompatibleWith(typeArgMustBeCompatibleWith);
8-
}
9+
10+
public string DescribeFor(object? argument) =>
11+
argument?.GetType().GetNonMangledTypeName() ?? "<null>" + " is not a " + typeArgMustBeCompatibleWith.GetNonMangledTypeName();
12+
13+
public string DescribeSpecification() => ToString();
14+
}

src/NSubstitute/Core/Arguments/ArgumentMatcher.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Diagnostics.CodeAnalysis;
12
using NSubstitute.Exceptions;
23

34
namespace NSubstitute.Core.Arguments;
@@ -53,6 +54,11 @@ public GenericToNonGenericMatcherProxyWithDescribe(IArgumentMatcher<T> matcher)
5354
}
5455

5556
public string DescribeFor(object? argument) => ((IDescribeNonMatches)_matcher).DescribeFor(argument);
57+
58+
public override string ToString() =>
59+
_matcher is IDescribeSpecification describe
60+
? describe.DescribeSpecification() ?? string.Empty
61+
: _matcher.ToString() ?? string.Empty;
5662
}
5763

5864
private class DefaultValueContainer<T>

src/NSubstitute/Core/Arguments/EqualsArgumentMatcher.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,11 @@ public class EqualsArgumentMatcher(object? value) : IArgumentMatcher
55
public override string ToString() => ArgumentFormatter.Default.Format(value, false);
66

77
public bool IsSatisfiedBy(object? argument) => EqualityComparer<object>.Default.Equals(value, argument);
8+
}
9+
10+
public class TypedEqualsArgumentMatcher<T>(T? value) : IArgumentMatcher<T>
11+
{
12+
public override string ToString() => ArgumentFormatter.Default.Format(value, false);
13+
14+
public bool IsSatisfiedBy(T? argument) => EqualityComparer<T>.Default.Equals(argument, value);
815
}

tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using NSubstitute.Exceptions;
55
using NSubstitute.Extensions;
66
using NUnit.Framework;
7+
using static NSubstitute.Acceptance.Specs.Extensions;
8+
using static NSubstitute.ArgMatchers;
79

810
namespace NSubstitute.Acceptance.Specs;
911

@@ -12,6 +14,12 @@ public class ArgumentMatching
1214
{
1315
private ISomething _something;
1416

17+
[SetUp]
18+
public void SetUp()
19+
{
20+
_something = Substitute.For<ISomething>();
21+
}
22+
1523
[Test]
1624
public void Return_result_for_any_argument()
1725
{
@@ -866,12 +874,6 @@ public void Does_support_out_method_with_base_override()
866874
Assert.That(outArg, Is.EqualTo(4));
867875
}
868876

869-
[SetUp]
870-
public void SetUp()
871-
{
872-
_something = Substitute.For<ISomething>();
873-
}
874-
875877
public interface IMyService
876878
{
877879
void MyMethod<T>(IMyArgument<T> argument);
@@ -919,6 +921,21 @@ public void Should_use_empty_string_for_null_describe_spec_for_custom_arg_matche
919921
Assert.That(ex.Message, Contains.Substring("Add(23, )"));
920922
}
921923

924+
[Test]
925+
public void Custom_arg_matcher_support()
926+
{
927+
_something.Add(1, 2);
928+
929+
_something.Received().Add(1, Arg.Is(GreaterThan(0)));
930+
931+
var exception = Assert.Throws<ReceivedCallsException>(() =>
932+
_something.Received().Add(1, Arg.Is(GreaterThan(3))));
933+
934+
Assert.That(exception.Message, Contains.Substring("Add(1, >3)"));
935+
Assert.That(exception.Message, Contains.Substring("Add(1, *2*)"));
936+
Assert.That(exception.Message, Contains.Substring("arg[1]: 2 \u226f 3"));
937+
}
938+
922939
class CustomMatcher : IArgumentMatcher, IDescribeNonMatches, IArgumentMatcher<int>
923940
{
924941
public string DescribeFor(object argument) => "failed";
@@ -956,4 +973,39 @@ public override int MethodWithOutParameter(int arg1, out int arg2)
956973
return 2;
957974
}
958975
}
976+
977+
#if NET6_0_OR_GREATER
978+
/// <summary>
979+
/// See https://github.com/nsubstitute/NSubstitute/issues/822.
980+
/// </summary>
981+
[Test]
982+
public void Predicate_match()
983+
{
984+
_something.Say("hello");
985+
986+
_something.Received().Say(Arg.Is(Matching<string>(x => x?.Length > 0)));
987+
988+
var exception = Assert.Throws<ReceivedCallsException>(() =>
989+
_something.Received().Say(Arg.Is(Matching<string>(x => x?.Length > 10))));
990+
Assert.That(exception.Message, Contains.Substring("Say(x => x?.Length > 10)"));
991+
Assert.That(exception.Message, Contains.Substring("Say(*\"hello\"*)"));
992+
}
993+
#endif
959994
}
995+
996+
static class Extensions
997+
{
998+
public static IArgumentMatcher<T> GreaterThan<T>(T value) where T : IComparable<T> =>
999+
new GreaterThanMatcher<T>(value);
1000+
1001+
private class GreaterThanMatcher<T>(T value) :
1002+
IDescribeNonMatches, IDescribeSpecification, IArgumentMatcher<T>
1003+
where T : IComparable<T>
1004+
{
1005+
public string DescribeFor(object argument) => $"{argument}{value}";
1006+
1007+
public string DescribeSpecification() => $">{value}";
1008+
1009+
public bool IsSatisfiedBy(T argument) => argument.CompareTo(value) > 0;
1010+
}
1011+
}

0 commit comments

Comments
 (0)