From 957259fd9fb215689ab00a4039af078e3dd94b35 Mon Sep 17 00:00:00 2001 From: David Tchepak Date: Sun, 5 May 2024 14:17:12 +1000 Subject: [PATCH] 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 #796. --- .../Core/Arguments/ArgumentMatcher.cs | 7 ++++ .../Core/Arguments/IArgumentMatcher.cs | 12 ++++--- src/NSubstitute/Core/CallSpecification.cs | 4 ++- src/NSubstitute/Core/IDescribeNonMatches.cs | 7 +++- .../Core/IDescribeSpecification.cs | 15 ++++++++ .../ArgumentMatching.cs | 35 ++++++++++++++++++- 6 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 src/NSubstitute/Core/IDescribeSpecification.cs diff --git a/src/NSubstitute/Core/Arguments/ArgumentMatcher.cs b/src/NSubstitute/Core/Arguments/ArgumentMatcher.cs index f4d4bbee..c82e52b8 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,9 @@ public GenericToNonGenericMatcherProxy(IArgumentMatcher matcher) } public bool IsSatisfiedBy(object? argument) => _matcher.IsSatisfiedBy((T?)argument!); + + public override string ToString() => + (_matcher as IDescribeSpecification)?.DescribeSpecification() ?? _matcher.ToString() ?? ""; } private class GenericToNonGenericMatcherProxyWithDescribe : GenericToNonGenericMatcherProxy, IDescribeNonMatches @@ -53,6 +57,9 @@ public GenericToNonGenericMatcherProxyWithDescribe(IArgumentMatcher matcher) } public string DescribeFor(object? argument) => ((IDescribeNonMatches)_matcher).DescribeFor(argument); + + public override string ToString() => + (_matcher as IDescribeSpecification)?.DescribeSpecification() ?? _matcher.ToString() ?? ""; } private class DefaultValueContainer 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..7f1f94d9 100644 --- a/src/NSubstitute/Core/CallSpecification.cs +++ b/src/NSubstitute/Core/CallSpecification.cs @@ -123,7 +123,9 @@ public IEnumerable 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); } 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..ffa92545 --- /dev/null +++ b/src/NSubstitute/Core/IDescribeSpecification.cs @@ -0,0 +1,15 @@ +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. + /// + /// + string DescribeSpecification(); +} diff --git a/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs b/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs index 63468c3d..3940a81b 100644 --- a/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs +++ b/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs @@ -745,4 +745,37 @@ 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())); + }); + Assert.That(ex.Message, Contains.Substring("Add(23, DescribeSpec)")); + } + + 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 : CustomMatcher, IDescribeSpecification + { + public string DescribeSpecification() => "DescribeSpec"; + } +}