如何使这个功能可测试?

Asi*_*sik 8 f# unit-testing 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. }
Run Code Online (Sandbox Code Playgroud)

我现在有单元测试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
Run Code Online (Sandbox Code Playgroud)

这似乎是Bastard Injection的功能等同物,但除此之外,我主要关心的是:

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

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

Gru*_*oon 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. }
Run Code Online (Sandbox Code Playgroud)

然后,在"生产"代码中,您注入了真正的依赖项:

module Production =         
    let applyUpdate updateData currentState = 
        Core.applyUpdate Real.getThis Real.getThat Real.createThatThingfn updateData currentState
Run Code Online (Sandbox Code Playgroud)

或者更简单地说,使用部分应用:

module Production =         
    let applyUpdate = 
        Core.applyUpdate Real.getThis Real.getThat Real.createThatThing
Run Code Online (Sandbox Code Playgroud)

在测试版本中,您将注入模拟或存根:

module Test =       
    let applyUpdate = 
        Core.applyUpdate Mock.getThis Mock.getThat Mock.createThatThing
Run Code Online (Sandbox Code Playgroud)

在上面的"生产"示例中,我静态地硬编码了Real函数的依赖关系,但是,或者,就像OO样式依赖注入一样,生产applyUpdate可以由一些顶级协调器创建,然后传递到需要它的函数中.

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

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

这种方法有更复杂的版本,例如"Reader"monad,但上面的代码是最简单的方法.

Mark Seemann在这个主题上有很多好的帖子,比如集成测试SOLID:下一步是功能端口和适配器.