如何使这个功能可测试?

时间:2016-07-29 17:00:25

标签: unit-testing f# functional-programming

我有一个函数负责收集一堆配置并从所有这些部分中进行更大的配置。所以它基本上是:

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. }

我目前对getThisgetThatcreateThatThing进行单元测试,但不对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的功能等同物,但除此之外,我主要关注的是:

  • 现在我的代码更难以遵循,因为你不能只是F12(Go 定义)到功能;这个问题也存在 OO依赖注入但通过工具减轻(即Resharper Go 实施)。
  • 我测试的功能在技术上并不是生产代码调用的功能(映射中可能存在错误)
  • 我甚至没有看到一个好名字" Testable"功能的版本
  • 我用一切重复的定义来污染模块

如何在函数式编程中处理这些问题?

2 个答案:

答案 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可以由一些顶级协调员创建 然后传递给需要它的函数。

这回答了你的问题,我希望:

  • 相同的核心代码用于生产和测试
  • 如果您静态地对依赖项进行硬编码,您仍然可以使用F12钻取它们。

此方法有更复杂的版本,例如“Reader”monad,但上面的代码是最简单的方法。

Mark Seemann在此主题上有很多好帖子,例如Integration TestingSOLID: the next step is Functional以及Ports and Adapters

答案 1 :(得分:3)

Scott(@Grundoon)的回答涵盖了从OOP到FP的更直接的翻译。如果您希望其中一个getThisgetThat函数不纯,那就合适了。

通常,将函数作为参数传递给其他函数是一件非常实用的事情(接收函数被称为高阶函数,然后),但它应该在感兴趣的情况下完成实现可变性。仅为测试目的添加额外的函数参数会导致David Heinemeier Hansson称之为test-induced damage

在这个答案中,我想提出另一个观点,虽然我想强调斯科特的答案符合我自己的想法(并且我已经赞成它)。它适合F#,因为F#是一种混合语言,并且隐含不纯函数是可能的。

在严格的函数式语言(如Haskell)中,默认情况下函数为pure。如果我们假设getThisgetThat等等都是引用透明(纯),则函数调用可以用它们的返回值替换。

这意味着你不必用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,因此没有理由改变他们的设计以使其“可测试”。