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

问题描述 投票:5回答:2

这个问题基于一个很棒的F#/ DI相关帖子:https://fsharpforfunandprofit.com/posts/dependency-injection-1/

我试着在那里发布问题。但是,由于网站上的一些故障,似乎无法再注册帖子。所以,这里是:

我想知道该帖子中描述的场景将如何工作/转化为更真实的例子。下面的数字是从天空稍微有点,所以,请在需要时调整它们。

考虑一些相当小的基于C#的DI / TDD / EF Code First项目:

组合根:20个接口,每个接口有10个方法(平均)。好吧,这可能是每个接口的方法太多,但不幸的是,随着代码的发展,它们往往会膨胀。我见过更多。其中10个是没有任何IO的内部服务(func世界中没有数据库/“纯”函数),5个是内部IO(本地数据库和类似),最后5个是外部服务(如外部数据库) s)或其他任何调用某些远程第三方服务的东西)。

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

有几个级别的测试:单元测试,集成测试(两个级别),验收测试。

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

集成测试(级别1):所有没有IO的内部服务都是真实的,所有内部IO相关服务都是假货(通常是内存数据库),所有外部服务都代理一些假货/模拟。基本上这意味着所有内部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我不是在寻找确切的答案。有一些想法的“方向”就足够了。

c# entity-framework dependency-injection f# tdd
2个回答
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)中,人们喜欢使用一种称为“免费monad”的技术来完成上述操作,但在我看来,这种典型的描述往往过于复杂。 (即如果你知道一个免费的monad是什么,这可能是有用的指针,但除此之外,你可能最好不要进入这个兔子洞)。

4
投票

只是为了补充托马斯的优秀答案,这里有一些其他的建议。

为每个工作流程使用管道

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

这种方法的好处在于,这些管道中的每一个都可以独立设置,具有自己的组合根。

你说你有20个接口,每个接口有10个方法。每个工作流程都需要所有这些接口和方法吗?根据我的经验,单个工作流可能只需要其中的一些,在这种情况下,组合根中的逻辑变得更容易。

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

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”程序集的“核心”程序集,该程序集读取配置并设置外部服务。 “内部”组合根存在于“核心”组件中,“外部”组合根存在于“API”组件中。

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

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 
      ]
  ]   

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

© www.soinside.com 2019 - 2024. All rights reserved.