Capturing method arguments on your fakes (using FakeItEasy)

There are many isolation frameworks around that make writing unit tests relatively simple. It's highly unlikely that any single framework will meet all your needs, so writing your own utilities can be useful in order to make your tests easier to write and – perhaps more importantly – read.

I regularly find myself wanting to verify that a certain dependency is called in the right way by my system under test. This includes passed argument values, which can be complex objects themselves.

Suppose I want to verify that my fictitious SUT Circle.CalculateArea calls ICalculator.Square with the radius of the circle. This would be pretty easy with FakeItEasy:

var calculator = A.Fake<ICalculator>(); 
A.CallTo(() => calculator.Square(3)).Returns(9);

var sut = new Circle(3, calculator);
double area = sut.CalculateArea();
Assert.AreEqual(9d, area);

Now imagine that this parameter value is a whole lot more complicated than a double. FakeItEasy provides a syntax for that too. Say I want to verify that the product being saved has the right name:

A.CallTo(
    () => repository.AddProduct(A<Product>.That.Matches(p => p.Name == "MyName")).
    Returns(productId);

Still pretty readable. However, you can imagine if the number of properties to verify grows, this becomes increasingly difficult to read. I prefer to use Assert-methods from my unit testing framework instead. While it's perfectly possible to put these statements into a method and use it when I configure my fake, it's starting to get messy:

A.CallTo(
    () => repository.AddProduct(A<Product>.That.Matches(p =>
    {
        Assert.AreEqual("MyName", p.Name);
        Assert.AreEqual(100m, p.Price);
        // Etcetera
        return true;
    }).
    Returns(productId);

A better solution: capturing the argument value

Rather than using this syntax, you can capture the value passed for the parameter instead. This makes it easier to write a test that follows Arrange-Act-Assert more closely as well. Using some background magic with a helper class it can be pretty easy to read:

// Arrange
const int productId = 42;
var sourceProduct = new Product { Name = "Source Product", Price = 100m }
var productArg = new Capture<Product>();
A.CallTo(() => repository.AddProduct(productArg).Returns(productId);

// Act
sut.DuplicateProduct(sourceProduct);

// Assert
var copy = productArg.Value;
Assert.AreEqual("Copy of Source Product", product.Name);
Assert.AreEqual(sourceProduct.Price, product.Price);

I hope you agree that this scales a lot better than a predicate.

How it works

There's a small amount of magic being done by the Capture<T> class. Take a look at the source code. The amount of code needed for the actual capture is a one-liner, but there is an implicit conversion that enables the terse syntax and then some checks to prevent you from shooting yourself in the foot. There's also a Values property for multiple captures, and HasValues as a shortcut for Values.Count > 0.

Note that the implementation is not thread-safe. Unit tests should avoid dealing with threading if at all possible, but if you need the class to be thread-safe, it is easily added.

How it really really works

For those taking a close look at the implementation: You may be wondering what happens in this case:

var productArg = new Capture<Product>();
A.CallTo(() => repository.SetProductStock(productArg, 0));
A.CallTo(() => repository.SetProductStock(A<Product>._, 1));

repository.SetProductStock(new Product(), 1);

I fully expected the product argument to be captured, even though the constraint on the second parameter is not met. Surprisingly, I could not reproduce this potential problem. FakeItEasy appears to do some deep magic to check the constraints without executing the predicate I'm passing. I've even tried some more complex cases where all constrains were predicates, but all of them seemed to work as desired. Going through the FakeItEasy code I still don't fully understand, so if you can either give me an example which breaks, or explain why it doesn't, I would be most grateful :)

Files:

Comments (5) -

  • That's very cool, Marcel.

    I've occasionally written one-off capturers, usually inside an Invokes, but this is  a nice example of a general-purpose version of a capturing parameter.

    A couple of notes. Your "embedded Assert" syntax is something that I've been people try before. It's a little problematic for reasons other than readability. As soon as multiple rules are added for a method, you have a very real chance that a call will cause one of the Asserts will fail, and that will stop execution before the other "rules" can be checked.

    There's no really deep magic about why your product wasn't captured at the end of your example. It's just that fake rules are applied like a stack, so the more recent rule overrides the earlier one. (See "Changing behavior between calls" at github.com/.../Limited-Call-Specifications for another explanation of the "stack" of rules.)

    So your

    A.CallTo(() => repository.SetProductStock(A<Product>._, 1));

    rule becomes the "active" (or at least "first checked") one. When you try to set the product stock with a count of 1, that rule matches, and the one with the capturing productArg is never tried at all.
    If you reverse the order of your A.CallTo()s, you'll see the test pass.

    • Hey Blair,

      Yeah, the embedded Assert syntax will abort execution of the rest of the test. That's always the case with Asserts however? Or are you referring to some case where the Assert might be expected not to fail?

      The order of the rules makes sense. I thought I tried that as well, but I guess I didn't, or not correctly. I'll try again... thanks Smile
      • Oh, no. Asserts always fail. The difference is that the usual FakeItEasy argument matchers do not cause a catastrophic failure if a non-matching argument comes in. They just don't match, and let another rule have a chance at it. The Assert will _shut you down_.

        I'l be keen to hear what you find about the rule ordering. (Spoiler alert: I actually tried it before I commented before; it wasn't just conjecture.)
  • Good and informative. I always visit this site. The actual difference is how the usual FakeItEasy argument matchers don't cause a catastrophic failure if your non-matching argument is available in. Thanks
  • Ted
    Very impressive syntax, thanks for the enlightenment.

Add comment

Loading