【问题标题】:Using Foq with F# function types将 Foq 与 F# 函数类型一起使用
【发布时间】:2018-02-18 18:51:39
【问题描述】:

我正在使用 F# 类型定义来防止我的函数之间的硬依赖,例如

type IType1 = int -> int
type IType2 = int-> string

let func1 (i : int) : int = i * i
let func2 (i : int) : string = i |> string

let higherFunc (dep1 : IType1) (dep2 : IType2) (input : int) : string =
    input |> dep1 |> dep2

let curriedFunc = higherFunc func1 func2
let x = curriedFunc 2

输出 x : "4"

显然,这是相当做作和简单的,但想象一下依赖项是解析器和排序器或其他任何东西。我正在编写的更小粒度的功能。

我正在尝试使用 Foq 来帮助我的单元测试装置。这是我正确使用 F# 的第一周,我很难弄清楚如何配置这些类型的模拟。

有两点值得一提:

1 - 如果我使用抽象类,我可以让它工作,但我不想这样做,因为它对于完全相同的最终结果要麻烦得多。例如

type IType1 = 
    abstract member doSomething : int -> int

type func1 () =
    interface IType1 with
        member this.doSomething (i: int) = i * i

允许我设置一个类似的模拟

let mT1= Mock.With (fun (x : IType1) -> <@ x.doSomething(any()) --> 5 @>)

但我真的不想这样做。

2 - 如果我只是使用

type IType1 = int -> int
let mT1 = Mock.Of<IType1>()

然后我会返回一个有效值,但如果我尝试以任何方式配置它

let mT1= Mock<IType1>.With (fun x -> <@ x(any()) --> 5 @>)

let mT1= Mock<IType1>.With (fun x -> <@ any() --> 5@>)

然后我得到一个例外

System.NotSupportedException : Expected standard function application: Call 

System.NotSupportedException : Expected standard function application: ValueWithName 

我希望我只是在语法上很愚蠢,并且可以做我想做的事。我已经尝试了我能想到的所有变体,包括 .Setup(conditions).Create() 的变体,但我在源代码中找不到任何示例。

我显然可以轻松地制作自己的模拟

let mT1 (i : int) : int = 5

因为任何适合该 int -> int 签名的东西都是有效的,但是如果我想检查该函数是否为 i 传递了某个值,我必须放入一个日志记录步骤等。它只是很高兴让 Foq 来做一些繁重的工作。

编辑 我刚刚注意到根 Mock 对象的签名中有“需要引用类型”(即 Mock )——这是否意味着我没有机会模拟值?如果我不配置 mock,它如何管理它?

【问题讨论】:

  • 我应该说我知道我不需要注释所有类型(也不需要在我的代码中),为了清楚起见,我只是在这里做了
  • 我在 Foq 存储库中创建了一个问题,地址为 github.com/fsprojects/Foq/issues/34

标签: .net unit-testing f# mocking foq


【解决方案1】:

你不必嘲笑。如果你的依赖只是函数类型,你可以只提供函数:

let mT1= fun x -> 5

对象模拟的整个概念是(必须)由面向对象的人发明的,以弥补对象组合不好(或根本不组合)的事实。当您的整个系统正常运行时,您可以在现场创建功能。无需嘲笑。

如果您真的沉迷于使用 Foq 的工具,例如日志记录和验证(我敦促您重新考虑:您的测试会更容易且更有弹性),您总是可以让自己成为一个可以充当代理的对象主持您的活动:

type ISurrogate<'t, 'r> =
    abstract member f: 't -> 'r

// Setup
let mT1 = Mock.Create<ISurrogate<int, int>>()
mT1.Setup(...)...

let mT2 = Mock.Create<ISurrogate<int, string>>()
mT2.Setup...

higherFunc mT1.f mT2.f 42

mT1.Received(1).Call( ... ) // Verification

这样,丑陋的地方就只限于你的测试,不会让你的生产代码复杂化。

显然,这仅适用于单参数函数。对于具有多个 curried 参数的函数,您必须对参数进行元组化并将调用包装在注入位置的 lambda 中:

// Setup
let mT1 = Mock.Create<ISurrogate<int * int, int>>()

higherFunc (fun x y -> mT1.f(x, y)) ...

如果您发现这种情况经常发生,您可以打包 lambda 创建以供重复使用:

let inject (s: ISurrogate<_,_>) x y = s.f (x,y)

higherFunc (inject mT1) ...

【讨论】:

  • 我实际上在我的帖子中通过最后一个代码示例解决了这个问题。我知道我可以做到这一点,关键是如果我想检查我的函数是否传递了正确的参数,或者被调用了一定次数,我必须写那些东西。那不是世界末日,这就是我现在正在做的事情,但是如果/当我想使用时,使用 Foq 会很好。像我们都提到的那种简单的假货很容易,但更丰富的模拟需要更多的努力。
  • 是的,我只是将其添加到答案中。请查看更新。
  • r.e.编辑 - 再次,我意识到我可以使用抽象类来做到这一点,但我试图坚持使用函数值类型,因为语法更简洁并且不涉及任何对象。我知道在实现等方面测试行为是个好主意,但是 - 例如,我有一个函数,它使用来自内部函数的 api 密钥,该函数构建原始字符串并将其传递给作为依赖项的哈希器。验证哈希是否传递了有效字符串的唯一方法是 a.) 模拟它并记录输入或 b.) 取消嵌套 api-key 构建器函数以使其公开
  • 事实证明,仔细的重构几乎消除了我需要创建 Foq 模拟的所有情况,而不仅仅是编写一个快速函数来传递给我的测试。有一次我确实使用它是检查是否使用正确的 args 调用了散列函数,因为它节省了我编写日志步骤的时间,但这真的很简单。最终,当我从 OO 大脑提出的更大的聚合函数转向更小、更集中的函数时,测试变得更加简单,正如 Fyodor 所建议的那样。
  • @Arwin - 您仍然可以通过合同进行设计。它甚至比 OO 更容易,因为类型很容易使用。我找到并修改了一个非常简单的静态 IOC 容器,以针对它们的类​​型注册我的 func 值(不是对象) - 这是一个要点gist.github.com/RyanBuzzInteractive/…
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-05-29
  • 2023-04-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多