diff --git a/src/NSubstitute/Core/Arguments/ArgumentMatcher.cs b/src/NSubstitute/Core/Arguments/ArgumentMatcher.cs index f4d4bbee..ac2315d3 100644 --- a/src/NSubstitute/Core/Arguments/ArgumentMatcher.cs +++ b/src/NSubstitute/Core/Arguments/ArgumentMatcher.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using NSubstitute.Exceptions; namespace NSubstitute.Core.Arguments; @@ -43,6 +44,11 @@ public GenericToNonGenericMatcherProxy(IArgumentMatcher 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 : GenericToNonGenericMatcherProxy, IDescribeNonMatches diff --git a/src/NSubstitute/Core/Arguments/ArgumentSpecification.cs b/src/NSubstitute/Core/Arguments/ArgumentSpecification.cs index 67147f0b..1c14274a 100644 --- a/src/NSubstitute/Core/Arguments/ArgumentSpecification.cs +++ b/src/NSubstitute/Core/Arguments/ArgumentSpecification.cs @@ -56,7 +56,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) { diff --git a/src/NSubstitute/Core/Arguments/IArgumentMatcher.cs b/src/NSubstitute/Core/Arguments/IArgumentMatcher.cs index b0000d89..e0a0f530 100644 --- a/src/NSubstitute/Core/Arguments/IArgumentMatcher.cs +++ b/src/NSubstitute/Core/Arguments/IArgumentMatcher.cs @@ -1,8 +1,10 @@ namespace NSubstitute.Core.Arguments; /// -/// Provides a specification for arguments for use with . -/// Can additionally implement to give descriptions when arguments do not match. +/// Provides a specification for arguments. +/// Can implement to give descriptions when arguments do not match. +/// Can implement to give descriptions of expected arguments (otherwise +/// `ToString()` will be used for descriptions). /// public interface IArgumentMatcher { @@ -14,8 +16,10 @@ public interface IArgumentMatcher } /// -/// Provides a specification for arguments for use with . -/// Can additionally implement to give descriptions when arguments do not match. +/// Provides a specification for arguments. +/// Can implement to give descriptions when arguments do not match. +/// Can implement to give descriptions of expected arguments (otherwise +/// `ToString()` will be used for descriptions). /// /// Matches arguments of type or compatible type. public interface IArgumentMatcher diff --git a/src/NSubstitute/Core/CallSpecification.cs b/src/NSubstitute/Core/CallSpecification.cs index 0dfa04cf..50cea538 100644 --- a/src/NSubstitute/Core/CallSpecification.cs +++ b/src/NSubstitute/Core/CallSpecification.cs @@ -123,7 +123,11 @@ public IEnumerable 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); } diff --git a/src/NSubstitute/Core/IDescribeNonMatches.cs b/src/NSubstitute/Core/IDescribeNonMatches.cs index d8ba00aa..94814ce6 100644 --- a/src/NSubstitute/Core/IDescribeNonMatches.cs +++ b/src/NSubstitute/Core/IDescribeNonMatches.cs @@ -1,5 +1,10 @@ namespace NSubstitute.Core; +/// +/// A type that can describe how an argument does not match a required condition. +/// Use in conjunction with to provide information about +/// non-matches. +/// public interface IDescribeNonMatches { /// @@ -9,4 +14,4 @@ public interface IDescribeNonMatches /// /// Description of the non-match, or if no description can be provided. string DescribeFor(object? argument); -} \ No newline at end of file +} diff --git a/src/NSubstitute/Core/IDescribeSpecification.cs b/src/NSubstitute/Core/IDescribeSpecification.cs new file mode 100644 index 00000000..b6d30765 --- /dev/null +++ b/src/NSubstitute/Core/IDescribeSpecification.cs @@ -0,0 +1,16 @@ +namespace NSubstitute.Core; + +/// +/// A type that can describe the required conditions to meet a specification. +/// Use in conjunction with to provide information about +/// what it requires to match an argument. +/// +public interface IDescribeSpecification +{ + /// + /// A concise description of the conditions required to match this specification, or + /// if a detailed description can not be provided. + /// + /// Description of the specification, or if no description can be provided. + string DescribeSpecification(); +} diff --git a/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs b/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs index 63468c3d..78ad814e 100644 --- a/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs +++ b/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs @@ -745,4 +745,47 @@ public void SetUp() { _something = Substitute.For(); } -} \ No newline at end of file + + [Test] + public void Should_use_ToString_to_describe_custom_arg_matcher_without_DescribesSpec() + { + var ex = Assert.Throws(() => + { + _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(() => + { + _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(() => + { + _something.Received().Add(23, ArgumentMatcher.Enqueue(new CustomDescribeSpecMatcher(null))); + }); + Assert.That(ex.Message, Contains.Substring("Add(23, )")); + } + + class CustomMatcher : IArgumentMatcher, IDescribeNonMatches, IArgumentMatcher + { + 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; + } +}