NSubstitute
NSubstitute copied to clipboard
Argument Capture
Hi. There is a feature in Mockito (java) and probably other mocking frameworks, where you can capture the arguments that were sent to a substitute function. This allows the developer to assert the arguments sent, after we interact with the substitute.
I did not find anything similar in this project, Do you have anything like that ?
I also replied on this SO thread, If it explains it better https://stackoverflow.com/questions/31316550/nsubstitute-check-arguments-passed-to-method
Example:
public interface IFoo { void DoSomthing(string stringArg); }
public class ArgCapture<T>
{
private List<T> m_arguments = new List<T>();
public T capture()
{
//T res = Arg.Is<T>(obj => add(obj)); >C#6
//T res = Arg.Compat.Is<T>(obj => add(obj)); <=C#6
T res = Arg.Compat.Is<T>(obj => add(obj));
return res;
}
public int Count
{
get { return m_arguments.Count; }
}
public T this[int index]
{
get { return m_arguments[index]; }
}
public List<T> Values
{
get { return new List<T>(m_arguments); }
}
private bool add(T obj)
{
m_arguments.Add(obj);
return true;
}
}
public class TestClass
{
[Test]
public void ArgCaptureTest()
{
IFoo foo1 = Substitute.For<IFoo>();
ArgCapture<string> stringArgCapture = new ArgCapture<string>();
foo1.DoSomthing("firstCall");
foo1.DoSomthing("secondCall");
foo1.Received(2).DoSomthing(stringArgCapture.capture());
Assert.AreEqual(2, stringArgCapture.Count);
Assert.AreEqual("firstCall", stringArgCapture[0]);
Assert.AreEqual("secondCall", stringArgCapture[1]);
}
}
Hi @megedsMprest ,
You can use Arg.Do for this. See Actions with arguments for an example. That needs to be set up in advance (before the calls, rather than after checking Received), although you can also use sub.ReceivedCalls() to query all calls and arguments passed (this is more fiddly than Arg.Do though).
Hope this helps.
This gets a bit more interesting when the argument is a lambda that is being used by the method under-test. I find myself fairly often in scenarios like the following:
// code under test:
public class Api
{
readonly IPersistence _persistence;
public Task DeleteValuesAsync(string requestor, ValueTarget pattern)
{
// other stuff
return _persistence.TransformAsync(requestor,
model => model.RemoveValues(pattern));
}
}
// where IPersistence.TransformAsync() basically loads a model,
// applies the passed transformer and saves it
// the signature of the method looks like:
Task TransformAsync(string requestor, Func<IRoot, IRoot> transformer);
Now in my test for Api.DeleteValuesAsync() I want to check that Api passes the correct lambda to IPersistence. So I write:
[Test]
public async Task DeleteValuesAsync_delegates_to_model_and_persistence()
{
// arrange
Func<IRoot, IRoot> receivedTransformer = null;
// Persistence is a substitute of IPersistence
await Persistence.TransformAsync(Requestor,
Arg.Do<Func<IRoot, IRoot>>(
a => receivedTransformer = a));
var target = SomeValueTarget;
// act
await _underTest.DeleteValuesAsync(Requestor, target);
// assert
var input = Substitute.For<IRoot>();
receivedTransformer(input);
input.Received().RemoveValues(target);
}
This works fine enough.
The setup in arrange is a bit awkward, though, especially because I have to repeat the Func<IRoot, IRoot> twice.
It would be nicer if I could write the arrange-part like this:
// arrange
Func<IRoot, IRoot> receivedTransformer = null;
await Persistence.TransformAsync(Requestor, Arg.Capture(ref receivedTransformer));
I've tried in the past creating an implementation of IArgumentMatcher<T> to do this, but failed :/
On the other hand, this is also similar to what Arg.Invoke does, only that it's a function, not an action. So it would be even nicer if I could write something like:
// arrange
var input = Substitute.For<IRoot>();
await Persistence.TransformAsync(Requestor,
Arg.InvokeWithArgument<Func<IRoot, IRoot>>(input);
var target = SomeValueTarget;
// act
await _underTest.DeleteValuesAsync(Requestor, target);
// assert
input.Received().RemoveValues(target);
This would actually be perfect - but I'm not sure if it's doable...
Actually, I played around a bit and have a solution. This is only for funcs with a single argument, but proves the principle:
public class LambdaExecutingMatcher<TArg, TResult> : IArgumentMatcher<Func<TArg, TResult>>
{
readonly TArg _argument;
public LambdaExecutingMatcher(TArg argument) => _argument = argument;
public bool IsSatisfiedBy(Func<TArg, TResult> lambda)
{
lambda(_argument);
return true;
}
}
public static class LambdaArg
{
public static ref Func<TArg, TResult> InvokeWithArgument<TArg, TResult>(TArg argument) =>
ref ArgumentMatcher.Enqueue(new LambdaExecutingMatcher<TArg, TResult>(argument));
}
With this I can re-write the test from the example further up to:
[Test]
public async Task DeleteValuesAsync_delegates_to_model_and_persistence()
{
// arrange
var input = Substitute.For<IRoot>();
await Persistence.TransformAsync(Requestor, LambdaArg.InvokeWithArgument<IRoot, IRoot>(input));
var target = SomeValueTarget;
// act
await _underTest.DeleteValuesAsync(Requestor, target);
// assert
input.Received().RemoveValues(target);
}
Now the next question and that one's trickier: what if we also want to check that the method-under-test uses the return value of the lambda?
Expanding the example from further above:
public interface IPersistence
{
// loads IRoot from storage, applies the getter and returns the result
Task<T> GetAsync<T>(Func<IRoot, T> getter);
}
public class Api
{
readonly IPersistence _persistence;
public Task<int> GetCountAsync(ValueTarget pattern)
=> _persistence.GetAsync(model => model.Count(pattern));
}
My current way to test this is:
[Test]
public async Task GetCountAsync_delegates_to_model_and_persistence()
{
// arrange
var model = Substitute.For<IRoot>();
model.Count(SomeValueTarget).Returns(13);
Persistence.GetAsync(Arg.Any<Func<IRoot, int>>())
.Returns(ci => ci.Arg<Func<IRoot, int>>()(model));
// act
var result= await _underTest.GetCountAsync(SomeValueTarget);
// assert
result.Should().Be(13);
}
Again, the setup line for Persistence is cumbersome. Preferably, I'd write something like
Persistence.GetAsync(Arg.InvokeAndReturn<Func<IRoot, int>>(model));
But I don't see how I would connect the IArgumentMatcher implementation with setting up the return value.
Is this possible? How would I go about it?
Thanks, MR
I'm not sure this is possible; NSubstitute doesn't like having overlapping calls to if you're stubbing GetAsync at the same time as model.Count then I think you will have a bad time. :(
Whenever I end up with complex setup like this I try to remove the mocking library from the equation and work out how I would test it all manually. That can some times give hints as to the best way to approach this (whether it be using a mocking lib for all or parts or none of the test).
For anyone interested this is the solution I cooked up which mostly align with the design approach of Mockito ArgumentCaptor (https://site.mockito.org/javadoc/current/org/mockito/ArgumentCaptor.html)
using System;
using System.Collections.Generic;
using System.Linq;
using NSubstitute;
public class ArgumentCaptor<T>
{
private readonly List<T> capturedValues = new();
public IReadOnlyList<T> CapturedValues => capturedValues.AsReadOnly();
public T GetCapturedValue() =>
capturedValues.LastOrDefault()
?? throw new InvalidOperationException($"No value captured for argument type '{typeof(T).Name}'");
public T CaptureArg()
{
#pragma warning disable NS1004
return Arg.Do<T>(arg => capturedValues.Add(arg));
#pragma warning restore NS1004
}
}
Usage example:
// Arrange
var dependency = new MyDependency();
var argCaptor = new ArgumentCaptor<ArgType>();
dependency.Operation(argCaptor.CaptureArg()).Returns(...)
var systemUnderTest = new SystemUnderTest(dependency);
// Act
systemUnderTest.OperationImTesting();
// Assert
var capturedArg = argCaptor.GetCapturedValue();
// Assert state of 'capturedArg'
Would have been nice with something similar built in such that we can retain the Arrange, Act, Assert test pattern. The suggested Arg.Do notation forces you to define you asserts in the 'arrange' section of your test code which somewhat breaks that pattern.
@AndreasPresthammer thanks, that is exactly what I was looking for 👍