首页 > 解决方案 > 真实项目的依赖注入的 F# 模拟

问题描述

该问题基于一篇很棒的 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-frameworkdependency-injectionf#tdd

解决方案


我认为答案取决于的一个关键问题是应用程序遵循的与外部 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 与程序逻辑交错太多,因此很难在没有某种形式的模拟的情况下对更大的逻辑组件进行任何测试。在这种情况下,Mark Seemann 的系列是一个很好的综合资源。我认为你的选择是:

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

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

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

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


推荐阅读