Skip to content

Capturing arguments

Often tests will make assertions against the calls made to a fake object to verify the behavior of the system under test, but you can also capture arguments supplied in a call to a fake object and examine the values later.

Simple capture

Take this contrived example:

"production code"
public interface IListLogger
{
    void Log(string message, IEnumerable<int> list);
}

internal sealed class Calculator(IListLogger logger)
{
    public int Add(IList<int> operands)
    {
        logger.Log("about to add", operands);
        return operands.Sum();
    }

    public int Square(int input)
    {
        logger.Log("about to square", [input]);
        return input * input;
    }
}

Suppose we want to make sure that we send correct messages to the logger. We can record the first argument to each Log call in a variable of type Captured and verify the values in the "assert" phase of the test.

simple capture
// Arrange
var capturedMessage = A.Captured<string>();

var logger = A.Fake<IListLogger>();
A.CallTo(() => logger.Log(
        capturedMessage._,
        An<IEnumerable<int>>._))
    .DoesNothing();

var calculator = new Calculator(logger);

// Act
calculator.Add([1, 2, 3, 4]);
calculator.Square(7);

// Assert
capturedMessage.Values.Should().Equal(
    "about to add",
    "about to square");

// Or if you only care about the last value:
capturedMessage.GetLastValue().Should().Be(
    "about to square");

This is a fairly standard test with fakes, except we:

  • create a Captured object to store received argument values
  • use the Captured object's _ member to configure the call to capture any values for that argument (this is analogous to the A<T>._ member, and just like it, there is a matching Ignored member in case you prefer that name)
  • specify that the method will do nothing. As with any attempt to configure a fake object's behavior, it's the final action that completes the A.CallTo sequence. Without it, there's no change to the fake object's behavior, and no values will be captured on calls to the method. You don't need to pair DoesNothing with configuration that uses a capturing argument: you could return values, throw exceptions, or whatever; it was just appropriate here for a method that returns nothing.
  • use Values to access all the captured values so they can be asserted
  • (alternative flow) use GetLastValue to access the most recent captured value so it can be asserted. This will throw a FakeItEasy.ExpectationException if no values have been captured.
Values are only captured if the call matches the configuration

When a call configuration intends to capture one or more arguments, the argument values are only captured if this specific call configuration is triggered. If an incoming call does not match that configured for the method or property, no arguments are captured.

Capturing and constraining at the same time

Even though you can interrogate captured values after the fact, you may want to configure fake behavior to take effect only when incoming arguments meet a constraint. As you might have guessed based on the references to _ and Ignored above, you can do this using a That method just like the one supplied by the A<T> argument constraints:

constrained capture
// Arrange
var capturedMessage = A.Captured<string>();

var logger = A.Fake<IListLogger>();
A.CallTo(() => logger.Log(
        capturedMessage.That.Contains("add"),
        An<IEnumerable<int>>._))
    .DoesNothing();

var calculator = new Calculator(logger);

// Act
calculator.Add([1, 2, 3, 4]);
calculator.Square(7);
calculator.Add([8, 9]);

// Assert
capturedMessage.Values.Should().Equal(
    "about to add",
    "about to add");

Capturing mutable arguments

The challenge

Argument capture is shallow; there's no copying of object state. If a reference-based argument (e.g. a class instance, not a struct) is captured and subsequently modified by the test or production code, the "assert" phase of the test will see the updated state.

capturing mutating values
// Arrange
var capturedOperands = A.Captured<IEnumerable<int>>();

var logger = A.Fake<IListLogger>();
A.CallTo(() => logger.Log(
        "about to add",
        capturedOperands._))
    .DoesNothing();

var calculator = new Calculator(logger);

// Act
List<int> operands = [1, 2, 3, 4];
calculator.Add(operands); // capturedOperands captures operands
operands.RemoveAt(0);
calculator.Add(operands); // captures operands again - same instance

// Assert
// passes:
capturedOperands.Values[1].Should().Equal(2, 3, 4);

// fails - operands contains only 2, 3, and 4:
capturedOperands.Values[0].Should().Equal(1, 2, 3, 4);

Here a single list instance is passed to the production code twice, but an element is removed between the calls. The Captured object captures a reference to the list each time, but does not preserve the internals of the list. So by the time the Values are examined, the list has had its first element removed, and this is reflected in the failing assertion.

It's the same effect as running

List<int> list = [1, 2, 3, 4];
var a = list;
list.RemoveAt(0);
var b = list;

// both a and b point to a list with elements { 2, 3, 4 }

Capturing frozen state

Captured objects can be created with a transforming function (or "freezer") that runs on the argument value before saving it away, thus insulating the captured values from subsequent mutations.

freezing state of captured values
// Arrange
var capturedOperands =
    A.Captured<IEnumerable<int>>().FrozenBy(l => l.ToList());

var logger = A.Fake<IListLogger>();
A.CallTo(() => logger.Log(
        "about to add",
        capturedOperands._))
    .DoesNothing();

var calculator = new Calculator(logger);

// Act
List<int> operands = [1, 2, 3, 4];
calculator.Add(operands); // capturedOperands captures copy of operands
operands.RemoveAt(0);
calculator.Add(operands); // capturedOperands captures copy of operands

// Assert
// both pass:
capturedOperands.Values[1].Should().Equal(2, 3, 4);
capturedOperands.Values[0].Should().Equal(1, 2, 3, 4);

You can even transform values into a different type:

freezing state of captured values as new type
// Arrange
var capturedOperands =
    A.Captured<IEnumerable<int>>().FrozenBy(l => string.Join(" + ", l));

var logger = A.Fake<IListLogger>();
A.CallTo(() => logger.Log(
        "about to add",
        capturedOperands._))
    .DoesNothing();

var calculator = new Calculator(logger);

// Act
List<int> operands = [1, 2, 3, 4];
calculator.Add(operands); // capturedOperands captures transformed operands
operands.RemoveAt(0);
calculator.Add(operands); // capturedOperands captures transformed operands

// Assert
capturedOperands.Values[1].Should().Be("2 + 3 + 4");
capturedOperands.Values[0].Should().Be("1 + 2 + 3 + 4");

In this case we supply an updated freezer function as well as the Captured typeparams for both the argument value and the captured value types.