Faking Return value with F# and FakeItEasy
Asked Answered
S

1

6

I am trying to use FakeItEasy to mock an interface defined in C#

public interface IMyInterface
{
    int HeartbeatInterval { get; set; }
}

In the F# test i do

let myFake = A.Fake<IMyInterface>()

A.CallTo(fun () -> ((!myFake).HeartbeatInterval)).Returns(10) |> ignore

Running this in the test runner results in

System.ArgumentException Expression of type 'Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,System.Int32]' cannot be used for return type 'System.Int32'

In fact it would seem that it does this for any return type e.g. if HeartbeatInterval returned type of foo then the exception thrown would be for type foo instead of System.Int32.

Am i doing this wrong or is there some incompatibility between F# and FakeItEasy?

It has crossed my mind that using an Object Expression might be an easier way to go.

Shirt answered 22/11, 2017 at 16:5 Comment(0)
R
8

I would venture a hypothesis that the "Easy" in "FakeItEasy" stands for "Easy Over Simple". The library says it's "easy", and that is probably true if you're using it from C#. Because it is quite obviously designed for use with that language. But it is far from "simple", because it is using C#-specific syntactic tricks that are hidden from view and don't work in F# well.

The specific gotcha you're getting right now is a combination of two things: (1) F# functions are not the same as C# Func<T,R>, and (2) F# overload resolution rules are different from C#.

There are three overloads of CallTo - two of them take an Expression<_>, and the third one is a "catch-all", taking an object. In C#, if you call this method with a lambda-expression as argument, the compiler will try its best to convert the lambda-expression to an Expression<_> and call one of the specialized methods. F#, however, does not make this effort: F#'s support for C#-style Expression<_> is very limited, primarily focused on compatability with LINQ, and only kicks in when there are no alternatives. So in this case, F# chooses to call the CallTo(object) overload.

Next, what would the argument be? F# is a very strict and consistent language. Apart from some special interop cases, most F# expressions have a definite type, regardless of the context in which they appear. Specifically, an expression of the form fun() -> x will have type unit -> 'a, where 'a is the type of x. In other words, it's an F# function.

At runtime, F# functions are represented by the type FSharpFunc<T,R>, so that is what the compiler will pass to the CallTo(object) method, which will look at it and, unable to understand what the hell it is, throw an exception.

To fix it, you could make yourself a special version of CallTo (let's call it FsCallTo) that would force the F# compiler to convert your fun() -> x expression into an Expression<_>, then use that method instead of CallTo:

// WARNING: unverified code. Should work, but I haven't checked.
type A with
    // This is how you declare extension methods in F#
    static member FsCallTo( e: System.Linq.Expressions.Expression<System.Func<_,_>> ) = A.CallTo( e )

let myFake = A.Fake<IMyInterface>()

// Calling FsCallTo will force F# to generate an `Expression<_>`, 
// because that's what the method expects:
A.FsCallTo(fun () -> ((!myFake).HeartbeatInterval)).Returns(10) |> ignore

However, as you have absolutely correctly observed, this is way too much of a hassle for mocking up an interface, since F# already has a perfectly statically verifiable, runtime-cost-free, syntactically nice alternative in the form of object expressions:

let myFake = { new IMyInterface with 
                 member this.HeartbeatInterval = 10
                 member this.HeartbeatInterval with set _ = ()
             }

I would totally recommend going with them instead.

Recency answered 22/11, 2017 at 16:35 Comment(2)
There is if course the alternative of actually using that overload that takes object instead, completely avoiding the expressions. A.CallTo(someObject).WhenArguments...Chancy
@Patrik what would someObject be in that case?Recency

© 2022 - 2024 — McMap. All rights reserved.