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

Add ability to provide a 'do' action on Arg.Is #780

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
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
17 changes: 17 additions & 0 deletions src/NSubstitute/Arg.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ public static ref T Is<T>(Expression<Predicate<object>> predicate) where T : Any
return ref ArgumentMatcher.Enqueue<T>(new ExpressionArgumentMatcher<object>(predicate));
}

/// <summary>
/// Match argument that satisfies <paramref name="predicate"/> and use it to call the <paramref name="useArgument"/> function
/// whenever a matching call is or was made to the substitute.
/// If the <paramref name="predicate"/> throws an exception for an argument it will be treated as non-matching.
/// </summary>
public static ref T IsAndDo<T>(Expression<Predicate<T>> predicate, Action<T> useArgument)
{
return ref ArgumentMatcher.Enqueue<T>(new ExpressionArgumentMatcher<T>(predicate), x => useArgument((T)x!));
}

/// <summary>
/// Invoke any <see cref="Action"/> argument whenever a matching call is made to the substitute.
/// </summary>
Expand Down Expand Up @@ -164,6 +174,13 @@ public static class Compat
/// </summary>
public static AnyType Is<T>(Expression<Predicate<object>> predicate) where T : AnyType => Arg.Is<T>(predicate);

/// <summary>
/// Match argument that satisfies <paramref name="predicate"/> and use it to call the <paramref name="useArgument"/> function
/// whenever a matching call is made to the substitute.
/// If the <paramref name="predicate"/> throws an exception for an argument it will be treated as non-matching.
/// </summary>
public static T IsAndDo<T>(Expression<Predicate<T>> predicate, Action<T> useArgument) => Arg.IsAndDo<T>(predicate, useArgument);

/// <summary>
/// Invoke any <see cref="Action"/> argument whenever a matching call is made to the substitute.
/// This is provided for compatibility with older compilers --
Expand Down
7 changes: 7 additions & 0 deletions src/NSubstitute/Compatibility/CompatArg.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ private CompatArg() { }
/// </summary>
public T Is<T>(Expression<Predicate<T>> predicate) => Arg.Is(predicate);

/// <summary>
/// Match argument that satisfies <paramref name="predicate"/> and use it to call the <paramref name="useArgument"/> function
/// whenever a matching call is or was made to the substitute.
/// If the <paramref name="predicate"/> throws an exception for an argument it will be treated as non-matching.
/// </summary>
public static T IsAndDo<T>(Expression<Predicate<T>> predicate, Action<T> useArgument) => Arg.IsAndDo<T>(predicate, useArgument);

/// <summary>
/// Match argument that satisfies <paramref name="predicate"/>.
/// If the <paramref name="predicate"/> throws an exception for an argument it will be treated as non-matching.
Expand Down
43 changes: 43 additions & 0 deletions src/NSubstitute/Match.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using NSubstitute.Core.Arguments;
using System.Linq.Expressions;

namespace NSubstitute;

/// <summary>
/// Argument matcher allowing a match predicate and optional action to be called for each match to be specified separately.
/// </summary>
public class Match<T>
{
private Expression<Predicate<T>> predicate;
private Action<T> useArgument;

internal Match(Expression<Predicate<T>> predicate, Action<T> useArgument)
{
this.predicate = predicate;
this.useArgument = useArgument;
}

/// <summary>
/// The <paramref name="useArgument"/> function to be invoked
/// for each matching call made to the substitute.
/// </summary>
public Match<T> AndDo(Action<T> useArgument) => new Match<T>(predicate, x => { this.useArgument(x); useArgument(x); });

public static implicit operator T? (Match<T?> match)
{
return ArgumentMatcher.Enqueue<T>(
new ExpressionArgumentMatcher<T>(match.predicate),
x => match.useArgument((T?)x)
);
}
}

public static class Match
{
/// <summary>
/// Match argument that satisfies <paramref name="predicate"/>.
/// If the <paramref name="predicate"/> throws an exception for an argument it will be treated as non-matching.
/// </summary>
public static Match<T> When<T>(Expression<Predicate<T>> predicate) =>
new Match<T>(predicate, x => { });
}
12 changes: 12 additions & 0 deletions src/NSubstitute/Routing/Handlers/CheckReceivedCallsHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ public RouteAction Handle(ICall call)
_exceptionThrower.Throw(callSpecification, matchingCalls, relatedCalls, _requiredQuantity);
}

InvokePerArgumentActionsForMatchingCalls(callSpecification, matchingCalls);

return RouteAction.Continue();
}

private static void InvokePerArgumentActionsForMatchingCalls(ICallSpecification callSpecification, List<ICall> matchingCalls)
{
var callInfoFactory = new CallInfoFactory();

foreach (var matchingCall in matchingCalls)
{
callSpecification.InvokePerArgumentActions(callInfoFactory.Create(matchingCall));
}
}
}
26 changes: 26 additions & 0 deletions tests/NSubstitute.Acceptance.Specs/ArgDoFromMatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,32 @@ public void Should_call_action_with_each_matching_call()
Assert.That(stringArgs, Is.EqualTo(new[] { "hello", "world" }));
}

[Test]
public void Should_call_action_with_each_call_matching_predicate_using_isanddo()
{
var stringArgs = new List<string>();
_sub.Bar(Arg.IsAndDo<string>(x => x.StartsWith("h"), x => stringArgs.Add(x)), Arg.Any<int>(), _someObject);

_sub.Bar("hello", 1, _someObject);
_sub.Bar("hello2", 2, _someObject);
_sub.Bar("don't use this because call doesn't match", -123, _someObject);

Assert.That(stringArgs, Is.EqualTo(new[] { "hello", "hello2" }));
}

[Test]
public void Should_call_action_with_each_call_matching_predicate()
{
var stringArgs = new List<string>();
_sub.Bar(Match.When<string>(x => x.StartsWith("h")).AndDo(stringArgs.Add), Arg.Any<int>(), _someObject);

_sub.Bar("hello", 1, _someObject);
_sub.Bar("hello2", 2, _someObject);
_sub.Bar("don't use this because call doesn't match", -123, _someObject);

Assert.That(stringArgs, Is.EqualTo(new[] { "hello", "hello2" }));
}

[Test]
public void Arg_do_with_when_for_any_args()
{
Expand Down
36 changes: 36 additions & 0 deletions tests/NSubstitute.Acceptance.Specs/ReceivedCalls.cs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,40 @@ public void Throw_when_negative_min_range_given()
StringAssert.Contains("minInclusive must be >= 0, but was -1.", ex.Message);
}

[Test]
public void Should_call_action_for_each_call_matching_predicate_using_isanddo()
{
var suitCaseLuggage = new List<object[]>();

_car.StoreLuggage(new SuitCase());
_car.StoreLuggage(new SuitCase());
_car.StoreLuggage(new object());

_car.Received(2).StoreLuggage(
Arg.IsAndDo<object[]>(
x => x.All(l => l is SuitCase),
suitCaseLuggage.Add));

Assert.That(suitCaseLuggage, Has.Count.EqualTo(2));
}

[Test]
public void Should_call_action_for_each_call_matching_predicate()
{
var suitCaseLuggage = new List<object[]>();

_car.StoreLuggage(new SuitCase());
_car.StoreLuggage(new SuitCase());
_car.StoreLuggage(new object());

_car.Received(2).StoreLuggage(
Match.When<object[]>(
x => x.All(l => l is SuitCase))
.AndDo(suitCaseLuggage.Add));

Assert.That(suitCaseLuggage, Has.Count.EqualTo(2));
}

public interface ICar
{
void Start();
Expand All @@ -328,4 +362,6 @@ public interface ICar
float GetCapacityInLitres();
event Action Started;
}

public class SuitCase;
}