Skip to content

Commit 90d93ec

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. Relates to nsubstitute#796.
1 parent ab73157 commit 90d93ec

File tree

6 files changed

+74
-7
lines changed

6 files changed

+74
-7
lines changed

src/NSubstitute/Core/Arguments/ArgumentMatcher.cs

+6
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ private class GenericToNonGenericMatcherProxy<T>(IArgumentMatcher<T> matcher) :
3838
protected readonly IArgumentMatcher<T> _matcher = matcher;
3939

4040
public bool IsSatisfiedBy(object? argument) => _matcher.IsSatisfiedBy((T?)argument!);
41+
42+
public override string ToString() =>
43+
(_matcher as IDescribeSpecification)?.DescribeSpecification() ?? _matcher.ToString() ?? "";
4144
}
4245

4346
private class GenericToNonGenericMatcherProxyWithDescribe<T> : GenericToNonGenericMatcherProxy<T>, IDescribeNonMatches
@@ -48,6 +51,9 @@ public GenericToNonGenericMatcherProxyWithDescribe(IArgumentMatcher<T> matcher)
4851
}
4952

5053
public string DescribeFor(object? argument) => ((IDescribeNonMatches)_matcher).DescribeFor(argument);
54+
55+
public override string ToString() =>
56+
(_matcher as IDescribeSpecification)?.DescribeSpecification() ?? _matcher.ToString() ?? "";
5157
}
5258

5359
private class DefaultValueContainer<T>

src/NSubstitute/Core/Arguments/IArgumentMatcher.cs

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
namespace NSubstitute.Core.Arguments;
22

33
/// <summary>
4-
/// Provides a specification for arguments for use with <see ctype="Arg.Matches (IArgumentMatcher)" />.
5-
/// Can additionally implement <see cref="IDescribeNonMatches" /> to give descriptions when arguments do not match.
4+
/// Provides a specification for arguments.
5+
/// Can implement <see cref="IDescribeNonMatches" /> to give descriptions when arguments do not match.
6+
/// Can implement <see cref="IDescribeSpecification"/> to give descriptions of expected arguments (otherwise
7+
/// `ToString()` will be used for descriptions).
68
/// </summary>
79
public interface IArgumentMatcher
810
{
@@ -14,8 +16,10 @@ public interface IArgumentMatcher
1416
}
1517

1618
/// <summary>
17-
/// Provides a specification for arguments for use with <see ctype="Arg.Matches &lt; T &gt;(IArgumentMatcher)" />.
18-
/// Can additionally implement <see ctype="IDescribeNonMatches" /> to give descriptions when arguments do not match.
19+
/// Provides a specification for arguments.
20+
/// Can implement <see cref="IDescribeNonMatches" /> to give descriptions when arguments do not match.
21+
/// Can implement <see cref="IDescribeSpecification"/> to give descriptions of expected arguments (otherwise
22+
/// `ToString()` will be used for descriptions).
1923
/// </summary>
2024
/// <typeparam name="T">Matches arguments of type <typeparamref name="T"/> or compatible type.</typeparam>
2125
public interface IArgumentMatcher<T>

src/NSubstitute/Core/CallSpecification.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@ public IEnumerable<ArgumentMatchInfo> NonMatchingArguments(ICall call)
121121

122122
public override string ToString()
123123
{
124-
var argSpecsAsStrings = _argumentSpecifications.Select(x => x.ToString() ?? string.Empty).ToArray();
124+
var argSpecsAsStrings = _argumentSpecifications.Select(x =>
125+
(x as IDescribeSpecification)?.DescribeSpecification() ?? x.ToString() ?? string.Empty
126+
).ToArray();
125127
return CallFormatter.Default.Format(GetMethodInfo(), argSpecsAsStrings);
126128
}
127129

src/NSubstitute/Core/IDescribeNonMatches.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
namespace NSubstitute.Core;
22

3+
/// <summary>
4+
/// A type that can describe how an argument does not match a required condition.
5+
/// Use in conjunction with <see cref="NSubstitute.Core.Arguments.IArgumentMatcher"/> to provide information about
6+
/// non-matches.
7+
/// </summary>
38
public interface IDescribeNonMatches
49
{
510
/// <summary>
@@ -9,4 +14,4 @@ public interface IDescribeNonMatches
914
/// <param name="argument"></param>
1015
/// <returns>Description of the non-match, or <see cref="string.Empty" /> if no description can be provided.</returns>
1116
string DescribeFor(object? argument);
12-
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace NSubstitute.Core;
2+
3+
/// <summary>
4+
/// A type that can describe the required conditions to meet a specification.
5+
/// Use in conjunction with <see cref="NSubstitute.Core.Arguments.IArgumentMatcher"/> to provide information about
6+
/// what it requires to match an argument.
7+
/// </summary>
8+
public interface IDescribeSpecification
9+
{
10+
11+
/// <summary>
12+
/// A concise description of the conditions required to match this specification.
13+
/// </summary>
14+
/// <returns></returns>
15+
string DescribeSpecification();
16+
}

tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs

+35-1
Original file line numberDiff line numberDiff line change
@@ -838,11 +838,45 @@ public interface IMyService
838838
{
839839
void MyMethod<T>(IMyArgument<T> argument);
840840
}
841+
841842
public interface IMyArgument<T> { }
842843
public class SampleClass { }
843844
public class MyStringArgument : IMyArgument<string> { }
844845
public class MyOtherStringArgument : IMyArgument<string> { }
845846
public class MySampleClassArgument : IMyArgument<SampleClass> { }
846847
public class MyOtherSampleClassArgument : IMyArgument<SampleClass> { }
847848
public class MySampleDerivedClassArgument : MySampleClassArgument { }
848-
}
849+
850+
[Test]
851+
public void Should_use_ToString_to_describe_custom_arg_matcher_without_DescribesSpec()
852+
{
853+
var ex = Assert.Throws<ReceivedCallsException>(() =>
854+
{
855+
_something.Received().Add(23, ArgumentMatcher.Enqueue(new CustomMatcher()));
856+
});
857+
Assert.That(ex.Message, Contains.Substring("Add(23, Custom match)"));
858+
}
859+
860+
[Test]
861+
public void Should_describe_spec_for_custom_arg_matcher_when_implemented()
862+
{
863+
var ex = Assert.Throws<ReceivedCallsException>(() =>
864+
{
865+
_something.Received().Add(23, ArgumentMatcher.Enqueue(new CustomDescribeSpecMatcher()));
866+
});
867+
Assert.That(ex.Message, Contains.Substring("Add(23, DescribeSpec)"));
868+
}
869+
870+
class CustomMatcher : IArgumentMatcher, IDescribeNonMatches, IArgumentMatcher<int>
871+
{
872+
public string DescribeFor(object argument) => "failed";
873+
public bool IsSatisfiedBy(object argument) => false;
874+
public bool IsSatisfiedBy(int argument) => false;
875+
public override string ToString() => "Custom match";
876+
}
877+
878+
class CustomDescribeSpecMatcher : CustomMatcher, IDescribeSpecification
879+
{
880+
public string DescribeSpecification() => "DescribeSpec";
881+
}
882+
}

0 commit comments

Comments
 (0)