How to test for a Match with FakeItEasy on a predicate call?
Asked Answered
B

2

6

I have the following call in my code:

var dbResults = new List<CrossReferenceRelationshipEF>();
dbResults = dateTimeFilter == null
    ? new List<CrossReferenceRelationshipEF>(
        CrossReferenceRelationshipRepository.GetAll()
                .ToList().OrderBy(crr => crr.ToPartner))
    : new List<CrossReferenceRelationshipEF>(
        CrossReferenceRelationshipRepository.SearchFor(
            crr => crr.HistoricEntries
                .Any(he => he.ModifiedDatetime > dateTimeFilter))
                .ToList().OrderBy(crr => crr.ToPartner));

and I am trying to use FakeItEasy to verify that when the dateTimeFilter has a value, the SearchFor(…) is being called within my repository with the correct Function.

So my test looks something like this:

A.CallTo(() => crossReferenceRelationshipRepositoryMock.SearchFor(A<Expression<Func<CrossReferenceRelationshipEF,bool>>>.That
    .Matches(exp => Expression.Lambda<Func<DateTime>>(((BinaryExpression)exp.Body).Right).Compile().Invoke() == filterByDate)))
    .MustHaveHappened(Repeated.Exactly.Once);

Which is not correct. What would be a way to test the whether or not I am calling SearchFor(…) with the correct expression?

crr => crr.HistoricEntries.Any(he => he.ModifiedDatetime > dateTimeFilter)

The actual value being passed into SearchFor(…) is DateTime.MinValue so I changed my assertion to:

A.CallTo(() => crossReferenceRelationshipRepositoryMock.SearchFor(A<Expression<Func<CrossReferenceRelationshipEF, bool>>>.That
    .Matches(exp => Expression.Lambda<Func<DateTime>>(((BinaryExpression)exp.Body).Right).Compile().Invoke() == DateTime.MinValue)))
    .MustHaveHappened(Repeated.Exactly.Once);

which is failing and the exception I am getting is

System.InvalidCastException:
  Unable to cast object of type 'System.Linq.Expressions.MethodCallExpressionN'
  to type 'System.Linq.Expressions.BinaryExpression'.

and I am not sure what I am doing wrong...

Brunei answered 29/1, 2014 at 16:35 Comment(0)
B
3

Sorry I should have answered this earlier. It is true that Blair Conrad and I had a chat and he helped me understand how to test the predicates better. Based on his recommendation I came up with the following solution.

In my tests I created a helper Expression extractor show below:

private static string ExpressionExtractor(Expression<Func<CrossReferenceRelationshipEF, bool>> predicate)
{
    var expression = ((BinaryExpression) ((LambdaExpression) ((MethodCallExpression) predicate.Body).Arguments[1]).Body);
    var value = Expression.Lambda<Func<object>>(Expression.Convert(expression.Right, typeof (object))).Compile().Invoke();

    return value.ToString();
}

And then in my tests I could do my assert like this:

//Assert        
A.CallTo(() => crossReferenceRelationshipRepositoryMock.SearchFor(A<Expression<Func<CrossReferenceRelationshipEF, bool>>>.That
    .Matches(exp => ExpressionExtractor(exp) == "20/01/2014 14:06:55")))
    .MustHaveHappened(Repeated.Exactly.Twice);
Brunei answered 22/4, 2014 at 10:16 Comment(0)
M
4

Disclosure - VasilisP and I chatted a little about this yesterday.

In a way, this isn't really a FakeItEasy problem. Your approach for setting up an argument matcher within an A.CallTo call is sound. The problem is that the lambda you supplied to match the predicate is not working. This brings the question down to the "how can I tell if an expression is what I want it to be?".

There are other StackOverflow questions that ask questions similar to this, such as

One of those approaches may work for you.

However, the immediate cause of the exception you see is that the passed-in predicate isn't a BinaryExpression, it's a MethodCallExpression. You could consider altering your test to account for that and follow the path where it leads you.

I filled in some class definitions and extracted the matcher to this function and was able to at least locate the dateArgument in the predicate:

public bool IsPredicateGood(Expression<Func<CrossReferenceRelationshipEF, bool>> predicate)
{
    var typedPredicate = (MethodCallExpression) predicate.Body;
    var innerPredicate = ((LambdaExpression)typedPredicate.Arguments[1]).Body;
    var dateArgument = ((BinaryExpression) innerPredicate).Right;
    return dateArgument != null; // not a real test yet, but you could adapt
}

In general, though, I'd warn against testing quite like this - it seems a little fragile to me. Of course, you may have a good reason for this approach. But if it suits you, another way to go may be to just capture the predicate and then interrogate it by having it run against a known list of candidate objects. If it filters the way you want, then it passes. That way if someone changes the passed-in predicate in a way that would still work, perhaps by switching the operator to a < with the date on the left, the test would still work. I just throw that out as another option.

Mullen answered 29/1, 2014 at 23:6 Comment(1)
Thanks for your help. I have posted my working solution below. Sorry it took me ages.Brunei
B
3

Sorry I should have answered this earlier. It is true that Blair Conrad and I had a chat and he helped me understand how to test the predicates better. Based on his recommendation I came up with the following solution.

In my tests I created a helper Expression extractor show below:

private static string ExpressionExtractor(Expression<Func<CrossReferenceRelationshipEF, bool>> predicate)
{
    var expression = ((BinaryExpression) ((LambdaExpression) ((MethodCallExpression) predicate.Body).Arguments[1]).Body);
    var value = Expression.Lambda<Func<object>>(Expression.Convert(expression.Right, typeof (object))).Compile().Invoke();

    return value.ToString();
}

And then in my tests I could do my assert like this:

//Assert        
A.CallTo(() => crossReferenceRelationshipRepositoryMock.SearchFor(A<Expression<Func<CrossReferenceRelationshipEF, bool>>>.That
    .Matches(exp => ExpressionExtractor(exp) == "20/01/2014 14:06:55")))
    .MustHaveHappened(Repeated.Exactly.Twice);
Brunei answered 22/4, 2014 at 10:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.