Skip to content

Commit d46f615

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. Closes nsubstitute#796.
1 parent 4139d6a commit d46f615

File tree

6 files changed

+73
-7
lines changed

6 files changed

+73
-7
lines changed

src/NSubstitute/Core/Arguments/ArgumentMatcher.cs

+7
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;
@@ -43,6 +44,9 @@ public GenericToNonGenericMatcherProxy(IArgumentMatcher<T> matcher)
4344
}
4445

4546
public bool IsSatisfiedBy(object? argument) => _matcher.IsSatisfiedBy((T?)argument!);
47+
48+
public override string ToString() =>
49+
(_matcher as IDescribeSpecification)?.DescribeSpecification() ?? _matcher.ToString() ?? "";
4650
}
4751

4852
private class GenericToNonGenericMatcherProxyWithDescribe<T> : GenericToNonGenericMatcherProxy<T>, IDescribeNonMatches
@@ -53,6 +57,9 @@ public GenericToNonGenericMatcherProxyWithDescribe(IArgumentMatcher<T> matcher)
5357
}
5458

5559
public string DescribeFor(object? argument) => ((IDescribeNonMatches)_matcher).DescribeFor(argument);
60+
61+
public override string ToString() =>
62+
(_matcher as IDescribeSpecification)?.DescribeSpecification() ?? _matcher.ToString() ?? "";
5663
}
5764

5865
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
@@ -123,7 +123,9 @@ public IEnumerable<ArgumentMatchInfo> NonMatchingArguments(ICall call)
123123

124124
public override string ToString()
125125
{
126-
var argSpecsAsStrings = _argumentSpecifications.Select(x => x.ToString() ?? string.Empty).ToArray();
126+
var argSpecsAsStrings = _argumentSpecifications.Select(x =>
127+
(x as IDescribeSpecification)?.DescribeSpecification() ?? x.ToString() ?? string.Empty
128+
).ToArray();
127129
return CallFormatter.Default.Format(GetMethodInfo(), argSpecsAsStrings);
128130
}
129131

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,15 @@
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+
/// <summary>
11+
/// A concise description of the conditions required to match this specification.
12+
/// </summary>
13+
/// <returns></returns>
14+
string DescribeSpecification();
15+
}

tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs

+34-1
Original file line numberDiff line numberDiff line change
@@ -745,4 +745,37 @@ public void SetUp()
745745
{
746746
_something = Substitute.For<ISomething>();
747747
}
748-
}
748+
749+
[Test]
750+
public void Should_use_ToString_to_describe_custom_arg_matcher_without_DescribesSpec()
751+
{
752+
var ex = Assert.Throws<ReceivedCallsException>(() =>
753+
{
754+
_something.Received().Add(23, ArgumentMatcher.Enqueue(new CustomMatcher()));
755+
});
756+
Assert.That(ex.Message, Contains.Substring("Add(23, Custom match)"));
757+
}
758+
759+
[Test]
760+
public void Should_describe_spec_for_custom_arg_matcher_when_implemented()
761+
{
762+
var ex = Assert.Throws<ReceivedCallsException>(() =>
763+
{
764+
_something.Received().Add(23, ArgumentMatcher.Enqueue(new CustomDescribeSpecMatcher()));
765+
});
766+
Assert.That(ex.Message, Contains.Substring("Add(23, DescribeSpec)"));
767+
}
768+
769+
class CustomMatcher : IArgumentMatcher, IDescribeNonMatches, IArgumentMatcher<int>
770+
{
771+
public string DescribeFor(object argument) => "failed";
772+
public bool IsSatisfiedBy(object argument) => false;
773+
public bool IsSatisfiedBy(int argument) => false;
774+
public override string ToString() => "Custom match";
775+
}
776+
777+
class CustomDescribeSpecMatcher : CustomMatcher, IDescribeSpecification
778+
{
779+
public string DescribeSpecification() => "DescribeSpec";
780+
}
781+
}

0 commit comments

Comments
 (0)