Why can't I capture a FakeItEasy expectation in a variable?
Asked Answered
C

3

7

I'm using FakeItEasy to fake some Entity Framework calls, to make sure a bunch of weird legacy database tables are getting mapped properly.

I need to assert that a Customer with an Invoice matching a specific DeliveryAddress is being added to the database.

If I do this:

A.CallTo(() => db.Customers.Add(
    A<Customer>.That.Matches(
        c => c.Invoices.First().Address == EXPECTED_ADDRESS)
    )
)).MustHaveHappened();

the code works perfectly. I want to streamline the syntax by moving the expectation elsewhere, but when i do this:

var expected = A<Customer>.That.Matches(
    c => c.Invoices.First().Address == EXPECTED_ADDRESS)
);
A.CallTo(() => db.Customers.Add(expected)).MustHaveHappened();

The test fails. What is happening inside the FakeItEasy code that means the expectation expression works when it's inline but can't be captured in a variable and reused later?

Chela answered 4/5, 2017 at 11:10 Comment(1)
It should fail by design: A<T>.That can only be used in the context of a call specification with A.CallTo()Headstream
P
5

The answer is in the docs at Always place Ignored and That inside A.CallTo:

The Ignored (and _) and That matchers must be placed within the expression inside the A.CallTo call. This is because these special constraint methods do not return an actual matcher object. They tell FakeItEasy how to match the parameter via a special event that's fired then the constraint method is invoked. FakeItEasy only listens to the events in the context of an A.CallTo.

I'm surprised the "test fails", though. What version are you using? As of FIE 2.0.0, using That as you did should throw an exception like

System.InvalidOperationException : A<T>.Ignored, A<T>._, and A<T>.That
can only be used in the context of a call specification with A.CallTo()
Phylactery answered 4/5, 2017 at 13:28 Comment(1)
I came here with a similar problem but the issue was missing "() =>" in the CallTo - very confusing error in this case A.CallTo(_service.PostAsync(A<string>.Ignored, A<IProduct>.Ignored)).Throws<HttpRequestException>(); <- that codes needs the delegate, compiles and runs without it, but throws this errorJennet
D
5

Not a direct answer for why the expectation expression works inline, but not in a variable (I'm working on that, will edit answer shortly!)

However, I'm not a fan of .That.Matches

The matches can get a bit unwieldy if there's more than one. Plus the MustHaveHappened call will throw an exception if any of the matches fail. Leaving me no idea where the failure happened.

I prefer to do this:

Customer addedCustomer;
A.CallTo(() => a.Add(A<Customer>._))
    .Invokes(c => addedCustomer = c.GetArgument<Customer>(0));

//Individual Asserts on addedCustomer
Assert.AreEqual(EXPECTED_ADDRESS, addedCustomer.Invoices.First().Address);
Dorseydorsiferous answered 4/5, 2017 at 11:25 Comment(3)
Capturing arguments for later interrogation is a useful technique. But can you explain what you mean by "Plus the MustHaveHappened call will throw an exception if any of the matches fail. Leaving me no idea where the failure happened.", either here or elsewhere? I'm wondering if there's an opportunity for improvement.Phylactery
@BlairConrad I think what he meant is that "Matches" takes predicate as an argument so if you want to compare many properties you'll end up with quite a big predicate. When MustHaveHappened throws an exception it will only tell you that the call you expected didn't happen and you won't know straight away which comparison failed.Filigreed
@BlairConrad I created an issue on github to avoid polluting this answer - github.com/FakeItEasy/FakeItEasy/issues/1078Dorseydorsiferous
P
5

The answer is in the docs at Always place Ignored and That inside A.CallTo:

The Ignored (and _) and That matchers must be placed within the expression inside the A.CallTo call. This is because these special constraint methods do not return an actual matcher object. They tell FakeItEasy how to match the parameter via a special event that's fired then the constraint method is invoked. FakeItEasy only listens to the events in the context of an A.CallTo.

I'm surprised the "test fails", though. What version are you using? As of FIE 2.0.0, using That as you did should throw an exception like

System.InvalidOperationException : A<T>.Ignored, A<T>._, and A<T>.That
can only be used in the context of a call specification with A.CallTo()
Phylactery answered 4/5, 2017 at 13:28 Comment(1)
I came here with a similar problem but the issue was missing "() =>" in the CallTo - very confusing error in this case A.CallTo(_service.PostAsync(A<string>.Ignored, A<IProduct>.Ignored)).Throws<HttpRequestException>(); <- that codes needs the delegate, compiles and runs without it, but throws this errorJennet
R
2

Blair's answer is correct. I just want to suggest a workaround:

// Local function, requires C# 7
Customer ExpectedCustomer() =>
    A<Customer>.That.Matches(
        c => c.Invoices.First().Address == EXPECTED_ADDRESS));

A.CallTo(() => db.Customers.Add(ExpectedCustomer())).MustHaveHappened();
Rectocele answered 4/5, 2017 at 14:24 Comment(3)
Looked very promising, although now I get an error An expression tree may not contain a reference to a local function. It's kind of sad that we're not allowed to do this.Wivestad
Oops. That's what you get for posting a solution without testing it. The limitations of expression trees are getting more and more annoying as C# gains more features...Rectocele
You can use a delegate, or a normal (non-local) method, thoughRectocele

© 2022 - 2024 — McMap. All rights reserved.