使用F#和FakeItEasy

时间:2017-11-22 16:05:04

标签: unit-testing f# fakeiteasy

我正在尝试使用FakeItEasy来模拟在C#中定义的接口

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

在F#测试中我做了

let myFake = A.Fake<IMyInterface>()

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

在测试运行器中运行此结果

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

事实上,它似乎是针对任何返回类型执行此操作,例如如果HeartbeatInterval返回foo类型,则抛出的异常将是类型foo而不是System.Int32

我做错了还是F#和FakeItEasy之间有些不兼容?

我已经想到使用对象表达式可能是一种更容易的方法。

1 个答案:

答案 0 :(得分:7)

我想冒一个假设,即&#34; Easy&#34;在&#34; FakeItEasy&#34;代表&#34; Easy Over Simple&#34;。图书馆表示它&#34; easy&#34;,如果你从C#中使用它,这可能就是真的。因为它非常明显地设计用于该语言。但它远非简单&#34;,因为它使用隐藏在视图中的C#特定语法技巧,并且不能在F#中工作。

您现在获得的具体问题是两件事的组合:(1)F#函数与C#Func<T,R>不同,(2)F#重载决策规则与C#不同

CallTo有三个重载 - 其中两个占用Expression<_>,第三个是&#34;全能&#34;,占用object。在C#中,如果使用lambda-expression作为参数调用此方法,编译器将尽力将lambda表达式转换为Expression<_>并调用其中一种专用方法。然而,F#没有做出这样的努力:F#对C#-style Expression<_>的支持非常有限,主要侧重于与LINQ的兼容性,并且只有在没有替代方案时才会启动。所以在这种情况下,F#选择调用CallTo(object)重载。

接下来,论证会是什么? F#是一种非常严格和一致的语言。除了一些特殊的互操作情况外,大多数F#表达式都有一个明确的类型,无论它们出现在何种上下文中。具体而言,表单fun() -> x的表达式将具有unit -> 'a类型,其中&{39; ax的类型。换句话说,它是一个F#函数。

在运行时,F#函数由类型FSharpFunc<T,R>表示,这就是编译器将传递给CallTo(object)方法的内容,它会查看它,并且无法理解它到底是什么是的,抛出异常。

要解决此问题,您可以使自己成为CallTo的特殊版本(让我们称之为FsCallTo),这会迫使F#编译器将您的fun() -> x表达式转换为Expression<_>,然后使用该方法代替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

然而,正如您已经完全正确地观察到的那样,这对于模拟接口来说太麻烦了,因为F#已经具有完全静态可验证,运行时成本免费,语法上不错对象表达式形式的替代方案:

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

我完全建议改用它们。