Skip to content

Commit

Permalink
Improve output for expected argument matchers
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
dtchepak committed May 5, 2024
1 parent 4139d6a commit d46f615
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 7 deletions.
7 changes: 7 additions & 0 deletions src/NSubstitute/Core/Arguments/ArgumentMatcher.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using NSubstitute.Exceptions;

namespace NSubstitute.Core.Arguments;
Expand Down Expand Up @@ -43,6 +44,9 @@ public GenericToNonGenericMatcherProxy(IArgumentMatcher<T> matcher)
}

public bool IsSatisfiedBy(object? argument) => _matcher.IsSatisfiedBy((T?)argument!);

public override string ToString() =>
(_matcher as IDescribeSpecification)?.DescribeSpecification() ?? _matcher.ToString() ?? "";
}

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

public string DescribeFor(object? argument) => ((IDescribeNonMatches)_matcher).DescribeFor(argument);

public override string ToString() =>
(_matcher as IDescribeSpecification)?.DescribeSpecification() ?? _matcher.ToString() ?? "";
}

private class DefaultValueContainer<T>
Expand Down
12 changes: 8 additions & 4 deletions src/NSubstitute/Core/Arguments/IArgumentMatcher.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
namespace NSubstitute.Core.Arguments;

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

/// <summary>
/// Provides a specification for arguments for use with <see ctype="Arg.Matches &lt; T &gt;(IArgumentMatcher)" />.
/// Can additionally implement <see ctype="IDescribeNonMatches" /> to give descriptions when arguments do not match.
/// Provides a specification for arguments. />.
/// Can implement <see cref="IDescribeNonMatches" /> to give descriptions when arguments do not match.
/// Can implement <see cref="IDescribeSpecification"/> to give descriptions of expected arguments (otherwise
/// `ToString()` will be used for descriptions).
/// </summary>
/// <typeparam name="T">Matches arguments of type <typeparamref name="T"/> or compatible type.</typeparam>
public interface IArgumentMatcher<T>
Expand Down
4 changes: 3 additions & 1 deletion src/NSubstitute/Core/CallSpecification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,9 @@ public IEnumerable<ArgumentMatchInfo> NonMatchingArguments(ICall call)

public override string ToString()
{
var argSpecsAsStrings = _argumentSpecifications.Select(x => x.ToString() ?? string.Empty).ToArray();
var argSpecsAsStrings = _argumentSpecifications.Select(x =>
(x as IDescribeSpecification)?.DescribeSpecification() ?? x.ToString() ?? string.Empty
).ToArray();
return CallFormatter.Default.Format(GetMethodInfo(), argSpecsAsStrings);
}

Expand Down
7 changes: 6 additions & 1 deletion src/NSubstitute/Core/IDescribeNonMatches.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
namespace NSubstitute.Core;

/// <summary>
/// A type that can describe how an argument does not match a required condition.
/// Use in conjunction with <see cref="NSubstitute.Core.Arguments.IArgumentMatcher"/> to provide information about
/// non-matches.
/// </summary>
public interface IDescribeNonMatches
{
/// <summary>
Expand All @@ -9,4 +14,4 @@ public interface IDescribeNonMatches
/// <param name="argument"></param>
/// <returns>Description of the non-match, or <see cref="string.Empty" /> if no description can be provided.</returns>
string DescribeFor(object? argument);
}
}
15 changes: 15 additions & 0 deletions src/NSubstitute/Core/IDescribeSpecification.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace NSubstitute.Core;

/// <summary>
/// A type that can describe the required conditions to meet a specification.
/// Use in conjunction with <see cref="NSubstitute.Core.Arguments.IArgumentMatcher"/> to provide information about
/// what it requires to match an argument.
/// </summary>
public interface IDescribeSpecification {

/// <summary>
/// A concise description of the conditions required to match this specification.
/// </summary>
/// <returns></returns>
string DescribeSpecification();
}
35 changes: 34 additions & 1 deletion tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs
Original file line number Diff line number Diff line change
Expand Up @@ -745,4 +745,37 @@ public void SetUp()
{
_something = Substitute.For<ISomething>();
}
}

[Test]
public void Should_use_ToString_to_describe_custom_arg_matcher_without_DescribesSpec()
{
var ex = Assert.Throws<ReceivedCallsException>(() =>
{
_something.Received().Add(23, ArgumentMatcher.Enqueue(new CustomMatcher()));
});
Assert.That(ex.Message, Contains.Substring("Add(23, Custom match)"));
}

[Test]
public void Should_describe_spec_for_custom_arg_matcher_when_implemented()
{
var ex = Assert.Throws<ReceivedCallsException>(() =>
{
_something.Received().Add(23, ArgumentMatcher.Enqueue(new CustomDescribeSpecMatcher()));
});
Assert.That(ex.Message, Contains.Substring("Add(23, DescribeSpec)"));
}

class CustomMatcher : IArgumentMatcher, IDescribeNonMatches, IArgumentMatcher<int>
{
public string DescribeFor(object argument) => "failed";
public bool IsSatisfiedBy(object argument) => false;
public bool IsSatisfiedBy(int argument) => false;
public override string ToString() => "Custom match";
}

class CustomDescribeSpecMatcher : CustomMatcher, IDescribeSpecification
{
public string DescribeSpecification() => "DescribeSpec";
}
}

0 comments on commit d46f615

Please sign in to comment.