我有一个函数负责收集一堆配置并从所有这些部分中进行更大的配置。所以它基本上是:
let applyUpdate updateData currentState =
if not (someConditionAbout updateData) then
log (SomeError)
let this = getThis updateData currentState.Thingy
let that = getThat updateData currentState.Thingy
let andThat = createThatThing that this updateData
// blablablablablabla
{ currentState with
This = this
That = that
AndThat = andThat
// etc. }
我目前对getThis
,getThat
,createThatThing
进行单元测试,但不对applyUpdate
进行单元测试。我不想重新测试getThis
等正在做的事情,我只是想测试特定于applyUpdate
的逻辑而只是测试存根getThis
。在面向对象的样式中,这些将通过依赖注入通过接口传递。在功能性风格中,我不确定如何继续:
// This is the function called by tests
let applyUpdateTestable getThisFn getThatFn createThatThingfn etc updateData currentState =
if not (someConditionAbout updateData) then
log (SomeError)
let this = getThisFn updateData currentState.Thingy
// etc
{ currentState with
This = this
// etc. }
// This is the function that is actually called by client code
let applyUpdate = applyUpdateTestable getThis getThat etc
这似乎是Bastard Injection的功能等同物,但除此之外,我主要关注的是:
如何在函数式编程中处理这些问题?
答案 0 :(得分:9)
你说:
在面向对象的样式中,这些将通过依赖注入通过接口传递。
在FP中使用相同的方法,但不是通过对象构造函数注入,而是“注入”作为函数的参数。
因此,您与applyUpdateTestable
走在正确的轨道上,除了这也将用作实际代码,而不仅仅是可测试的代码。
例如,这是传入三个额外依赖项的函数:
module Core =
let applyUpdate getThisFn getThatFn createThatThingfn updateData currentState =
if not (someConditionAbout updateData) then
log (SomeError)
let this = getThisFn updateData currentState.Thingy
// etc
{ currentState with
This = this
// etc. }
然后,在“生产”代码中,您将注入真正的依赖项:
module Production =
let applyUpdate updateData currentState =
Core.applyUpdate Real.getThis Real.getThat Real.createThatThingfn updateData currentState
或更简单地说,使用部分应用程序:
module Production =
let applyUpdate =
Core.applyUpdate Real.getThis Real.getThat Real.createThatThing
并且在测试版本中,您将注入模拟或存根:
module Test =
let applyUpdate =
Core.applyUpdate Mock.getThis Mock.getThat Mock.createThatThing
在上面的“生产”示例中,我静态地对Real
函数的依赖关系进行了硬编码,但是,
就像OO样式依赖注入一样,生产applyUpdate
可以由一些顶级协调员创建
然后传递给需要它的函数。
这回答了你的问题,我希望:
此方法有更复杂的版本,例如“Reader”monad,但上面的代码是最简单的方法。
Mark Seemann在此主题上有很多好帖子,例如Integration Testing和SOLID: the next step is Functional以及Ports and Adapters。
答案 1 :(得分:3)
Scott(@Grundoon)的回答涵盖了从OOP到FP的更直接的翻译。如果您希望其中一个getThis
,getThat
函数不纯,那就合适了。
通常,将函数作为参数传递给其他函数是一件非常实用的事情(接收函数被称为高阶函数,然后),但它应该在感兴趣的情况下完成实现可变性。仅为测试目的添加额外的函数参数会导致David Heinemeier Hansson称之为test-induced damage。
在这个答案中,我想提出另一个观点,虽然我想强调斯科特的答案符合我自己的想法(并且我已经赞成它)。它适合F#,因为F#是一种混合语言,并且隐含不纯函数是可能的。
在严格的函数式语言(如Haskell)中,默认情况下函数为pure。如果我们假设getThis
,getThat
等等都是引用透明(纯),则函数调用可以用它们的返回值替换。
这意味着你不必用Test Doubles替换它们。
相反,你可以简单地编写你的测试:
[<Fact>]
let testExample () =
// Create updateData and currentState values here...
let actual = applyUpdate updateData currentState
let expected =
{ currentState with
This = getThis updateData currentState.Thingy
That = getThat updateData currentState.Thingy
// etc. }
expected =! actual // assert that expected equals actual
你可能会说这个测试只复制了生产代码,但使用OO风格的测试双打的测试也是如此。我认为真正的问题比OP更复杂,这似乎并不能保证applyUpdate
函数的测试。
你也可以说这个测试不是单元测试,我同意语义;我将这些测试称为Facade Tests。
Pure functions are intrinsically testable,因此没有理由改变他们的设计以使其“可测试”。