真实项目的依赖注入的F#模拟

时间:2018-09-03 22:23:50

标签: c# entity-framework dependency-injection f# tdd

该问题基于与F#/ DI相关的出色文章:https://fsharpforfunandprofit.com/posts/dependency-injection-1/

我试图在此发布问题。但是,似乎由于站点上的某些故障,帖子无法再注册。因此,这里是:

我想知道那篇文章中描述的场景将如何工作/转化为更真实的示例。下面的数字离天空有点远,所以请根据需要进行调整。

考虑一些相当小的基于C#的DI / TDD / EF代码优先项目:

组成根:20个接口,每个接口平均10个方法。好的,每个接口可能有太多方法,但是不幸的是,随着代码的发展,它们通常会膨胀。我看到了更多。其中10个是没有任何IO的内部服务(func world中没有数据库/“纯”功能),5个是内部IO(本地数据库和类似数据库),后5个是外部服务(例如外部数据库( s)或调用远程第三方服务的其他任何东西。

每个接口都有一个生产级别的实现,具有4个注入接口(平均),每个接口使用5个成员,每个实现总共使用20种方法(平均)。

测试分为多个级别:单元测试,集成测试(两个级别),验收测试。

单元测试:所有调用均使用适当的模拟设置进行模拟(例如,使用某种标准工具,例如Moq)。因此,至少有20 * 10 = 200个单元测试。通常还有更多,因为要测试几种不同的情况。

集成测试(级别1):所有没有IO的内部服务都是真实的,所有与IO相关的内部IO服务都是伪造的(通常是内存DB),并且所有外部服务都带有某些伪造/模拟的代理。基本上,这意味着所有内部IO服务(例如SomeInternalIOService:ISomeInternalIOService)将被FakeSomeInternalIOService:ISomeInternalIOService替换,而所有外部IO服务(例如SomeExternalIOService:ISomeExternalIOService)将被FakeSomeExternalIOService:ISomeExternalIOService替换。因此,有5个虚假的内部IO和5个虚假的外部IO服务,并且测试数量与上述大致相同。

集成测试(级别2):所有外部服务(包括现在与本地数据库相关的外部服务)都是真实的,并且所有外部服务都被代理到其他一些伪造品/模拟物中,从而允许测试外部服务的失败。基本上,这意味着所有外部IO服务(例如SomeExternalIOService:ISomeExternalIOService)都将由BreakableFakeSomeExternalIOService:ISomeExternalIOService代替。有5种不同的(易碎)外部IO伪造服务。假设我们有大约100种这样的测试。

验收测试:一切都是真实的,但是配置文件指向外部服务的某些“测试”版本。假设有大约50种这样的测试。

我想知道这将如何转化为F#世界。显然,很多事情会非常与众不同,并且有些事情甚至在F#世界中甚至不存在

非常感谢!

PS我不是在寻找确切的答案。带有一些想法的“方向”就足够了。

2 个答案:

答案 0 :(得分:8)

我认为答案所依赖的一个关键问题是应用程序所遵循的与外部I / O的通信模式是什么以及控制交互的逻辑有多复杂。

在简单的情况下,您将具有以下内容:

+-----------+      +---------------+      +---------------+      +------------+
| Read data | ---> | Processing #1 | ---> | Processing #2 | ---> | Write data |
+-----------+      +---------------+      +---------------+      +------------+

在这种情况下,在设计精美的功能代码库中几乎不需要进行模拟。原因是您可以在没有任何I / O的情况下测试所有处理功能(它们只是需要一些数据并返回一些数据的功能)。至于读写,几乎没有什么可以进行实际测试的-它们大部分只是在做您在可模拟接口的“实际”实现中要做的工作。通常,您可以使读取和写入功能尽可能简单,并使处理功能具有所有逻辑。这是功能样式的最佳选择!

在更复杂的情况下,您将具有以下内容:

+----------+      +----------------+      +----------+      +------------+      +----------+
| Some I/O | ---> | A bit of logic | ---> | More I/O | ---> | More logic | ---> | More I/O |
+----------+      +----------------+      +----------+      +------------+      +----------+

在这种情况下,I / O与程序逻辑太交错了,因此如果没有某种形式的模拟,很难对较大的逻辑组件进行任何测试。在这种情况下,the series by Mark Seemann是很好的综合资源。我认为您的选择是:

  • 传递函数(并使用部分应用程序)-这是一种简单的函数方法,除非您需要传递太多参数,否则它将起作用。

  • 使用带有接口的更加面向对象的体系结构-F#是FP和OO的混合语言,因此它对此也有很好的支持。尤其是使用匿名接口实现意味着您通常不需要模拟库。

  • 使用“解释器”模式,其中以(嵌入式)领域特定的语言编写计算,该语言描述需要执行的计算和I / O(实际上无需执行)。然后,您可以在实际和测试模式下对DSL进行不同的解释。

  • 在某些功能语言(主要是Scala和Haskell)中,人们喜欢使用一种称为“自由monads”的技术来完成上述操作,但是在我看来,对此的典型描述往往过于复杂。 (即,如果您知道什么是免费的monad,这可能会很有帮助,但否则,最好不要进入这个兔子洞)。

答案 1 :(得分:4)

添加到Tomas的出色答案中,还有其他一些建议。

为每个工作流程使用管道

正如Tomas所说,在FP设计中,我们倾向于使用面向管道的设计,每个用例/工作流/场景都有一个管道。

这种方法的好处是,这些管道中的每一个都可以独立地设置,并使用它们自己的组合根。

您说您有20个接口,每个接口有10种方法。 每个工作流程都需要 all 这些接口和方法吗? 以我的经验,一个单独的工作流程可能只需要其中的一些,在这种情况下,合成根目录中的逻辑变得容易得多。

例如,如果工作流确实确实需要五个以上的参数,那么可能值得创建一个数据结构来保存这些依赖关系并将其传递给:

module BuyWorkflow =

    type Dependencies = {
       SaveSomething : Something -> AsyncResult<unit,DbError>
       LoadSomething : Key -> AsyncResult<Something,DbError>
       SendEmail : EmailMessage -> AsyncResult<unit,EmailError>
       ...
       }

    // define the workflow 
    let buySomething (deps:Dependencies) = 
        asyncResult {
           ...
           do! deps.SaveSomething ...
           let! something = deps.LoadSomething ...
        }

请注意,依赖关系通常只是单个功能,而不是整个接口。您只需要问问即可!

考虑具有多个“合成根”

您可能会考虑拥有多个“组成根”-一个用于内部服务,一个用于外部。

我通常将代码分解为仅包含纯代码和“ API”或“ WebService”程序集的“ Core”程序集 读取配置并设置外部服务。 “内部”合成词根位于“核心”程序集中,而“外部”合成词根位于“ API”程序集中。

例如,在“核心”程序集中,您可以有一个用于烘焙内部纯服务的模块。这是一些伪代码:

module Workflows =

    // set up pure services
    let internalServiceA = ...
    let internalServiceB = ...
    let internalServiceC = ...

    // set up workflows
    let homeWorkflow = homeWorkflow internalServiceA.method1 internalServiceA.method2 
    let buyWorkflow = buyWorkflow internalServiceB.method2 internalServiceC.method1 
    let sellWorkflow = ...

然后,将此模块用于“集成测试(级别1)”。 此时,工作流仍缺少它们的外部依赖性,因此您需要提供用于测试的模拟。

类似地,在“ API”程序集中,您可以具有一个提供外部服务的组合根。

module Api =

    // load from configuration
    let dbConnectionA = ...
    let dbConnectionB = ...

    // set up impure services
    let externalServiceA = externalServiceA(dbConnectionA)
    let externalServiceB = externalServiceB(dbConnectionB)
    let externalServiceC = ...

    // set up workflows
    let homeWorkflow = Workflows.homeWorkflow externalServiceA.method1 externalServiceA.method2 
    let buyWorkflow = Workflows.buyWorkflow externalServiceB.method2 externalServiceC.method1 
    let sellWorkflow = ...

然后在“集成测试(第2级)”和其他顶级代码中,使用Api工作流程:

// setup routes (using Suave/Giraffe style)
let routes : WebPart =
  choose [
    GET >=> choose [
      path "/" >=> Api.homeWorkflow 
      path "/buy" >=> Api.buyWorkflow 
      path "/sell" >=> Api.sellWorkflow 
      ]
  ]   

验收测试(具有不同的配置文件)可以使用相同的代码。