Skip to content

Commit

Permalink
Merge pull request #806 from dtchepak/improve-output-for-expected-arg…
Browse files Browse the repository at this point in the history
…-match

Improve output for expected argument matchers
  • Loading branch information
dtchepak authored Nov 30, 2024
2 parents ab73157 + 72005d0 commit 116db20
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 8 deletions.
5 changes: 5 additions & 0 deletions src/NSubstitute/Core/Arguments/ArgumentMatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ private class GenericToNonGenericMatcherProxy<T>(IArgumentMatcher<T> matcher) :
protected readonly IArgumentMatcher<T> _matcher = matcher;

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

public override string ToString() =>
_matcher is IDescribeSpecification describe
? describe.DescribeSpecification() ?? string.Empty
: _matcher.ToString() ?? string.Empty;
}

private class GenericToNonGenericMatcherProxyWithDescribe<T> : GenericToNonGenericMatcherProxy<T>, IDescribeNonMatches
Expand Down
5 changes: 4 additions & 1 deletion src/NSubstitute/Core/Arguments/ArgumentSpecification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ public string FormatArgument(object? argument)
: ArgumentFormatter.Default.Format(argument, highlight: !isSatisfiedByArg);
}

public override string ToString() => matcher.ToString() ?? string.Empty;
public override string ToString() =>
matcher is IDescribeSpecification describe
? describe.DescribeSpecification()
: matcher.ToString() ?? string.Empty;

public IArgumentSpecification CreateCopyMatchingAnyArgOfType(Type requiredType)
{
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
6 changes: 5 additions & 1 deletion src/NSubstitute/Core/CallSpecification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,11 @@ public IEnumerable<ArgumentMatchInfo> NonMatchingArguments(ICall call)

public override string ToString()
{
var argSpecsAsStrings = _argumentSpecifications.Select(x => x.ToString() ?? string.Empty).ToArray();
var argSpecsAsStrings = Array.ConvertAll(_argumentSpecifications, x =>
x is IDescribeSpecification describe
? describe.DescribeSpecification() ?? string.Empty
: x.ToString() ?? string.Empty
);
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);
}
}
16 changes: 16 additions & 0 deletions src/NSubstitute/Core/IDescribeSpecification.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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, or <see cref="string.Empty"/>
/// if a detailed description can not be provided.
/// </summary>
/// <returns>Description of the specification, or <see cref="string.Empty" /> if no description can be provided.</returns>
string DescribeSpecification();
}
46 changes: 45 additions & 1 deletion tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs
Original file line number Diff line number Diff line change
Expand Up @@ -838,11 +838,55 @@ public interface IMyService
{
void MyMethod<T>(IMyArgument<T> argument);
}

public interface IMyArgument<T> { }
public class SampleClass { }
public class MyStringArgument : IMyArgument<string> { }
public class MyOtherStringArgument : IMyArgument<string> { }
public class MySampleClassArgument : IMyArgument<SampleClass> { }
public class MyOtherSampleClassArgument : IMyArgument<SampleClass> { }
public class MySampleDerivedClassArgument : MySampleClassArgument { }
}

[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("DescribeSpec")));
});
Assert.That(ex.Message, Contains.Substring("Add(23, DescribeSpec)"));
}

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

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(string description) : CustomMatcher, IDescribeSpecification
{
public string DescribeSpecification() => description;
}
}

0 comments on commit 116db20

Please sign in to comment.