Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve output for expected argument matchers #806

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
}
Loading