pio*_*rek 8 testing haskell unit-testing functional-programming
在面向对象编程中我有对象和状态.所以我可以模拟对象的所有依赖项并测试对象.但函数式编程(特别是纯函数式)是关于组合函数的
它很容易测试不依赖于其他功能的功能.我们只是传递参数并检查结果.但是接受另一个函数并返回函数的函数呢?
假设我有代码g = h1 ? h2 ? h3 ? h4.我应该测试功能g吗?但那是集成/功能测试.仅使用集成测试来测试所有分支是不可能的.那么单元测试呢?当函数需要更多参数时,它会变得越来越复杂.
我应该创建自定义函数并将其用作模拟?不是昂贵且容易出错吗?
monads怎么样?例如,如何在haskell中测试控制台输出或磁盘操作?
我也一直在考虑在功能代码中进行测试。我没有所有的答案,但我会在这里写一点。
功能程序以不同的方式组合在一起,这需要不同的测试方法。
如果您粗略地看一下 Haskell 测试,您将不可避免地遇到 QuickCheck 和 SmallCheck,这两个非常有名的 Haskell 测试库。这些都进行“基于属性的测试”。
在面向对象语言中,您将费力地编写单独的测试来设置六个模拟对象,调用一两个方法,并验证是否使用正确的数据调用了预期的外部方法和/或方法最终返回了正确的答案。这是相当多的工作。您可能只对一两个测试用例执行此操作。
QuickCheck 是另一回事。您可能会编写一个属性,其内容类似于“如果我对此列表进行排序,则输出应具有与输入相同数量的元素”。这是一个单行。然后,QuickCheck 库将自动构建数百个随机生成的列表,并检查指定的条件是否适用于每个列表。如果没有,它会吐出测试失败的确切输入。
(QuickCheck 和 SmallCheck 做的事情大致相同。QuickCheck 生成随机测试,而 SmallCheck系统地尝试所有组合,直到指定的大小限制。)
您说您担心要测试的可能流控制路径的组合爆炸,但是使用此类工具为您动态生成测试用例,手动编写足够的测试不是问题。该唯一的问题是未来与足够的数据来测试所有的流动路径。
Haskell 也可以提供帮助。我读了一篇关于库的论文 [我不知道它是否曾经发布过],它实际上使用 Haskell 的惰性求值来检测被测代码对输入数据的处理。如在,它可以检测您正在测试的函数是查看列表的内容,还是仅查看该列表的大小。它可以检测正在触及该客户记录的哪些字段。等等。通过这种方式,它会自动生成数据,但不会浪费时间生成与此特定代码无关的数据部分的不同随机变化。(例如,如果您按 ID 对客户进行排序,则名称字段中的内容无关紧要。)
至于测试使用或生成函数的函数......是的,我没有答案。
在您的示例中,您可以分别测试 h1、h2、h3 和 h4,没问题,因为它们实际上并不相互依赖。也没有什么可以阻止您测试 g。但是 ga 是“单位”吗?Michael Feathers 在他著名的单元测试著作《有效地处理遗留代码》中给出了单元测试的一个很好的定义。他说,在构建管道的提交阶段运行单元测试既快速又可靠,并且足够快以供开发人员运行。所以 g 是这个度量的“单位”。关于单元测试的另一个优秀观点来自 Hexagonal Architecture,请参阅 TDD 哪里出错了?他们说你想通过它用来连接外部世界的“端口”来测试你的应用程序的 API。根据这个定义,你的 g 也是一个单位。但是他们所说的“端口”是什么意思 我们可以将其与 Haskell 联系起来吗?一个典型的端口可能是应用程序用来在数据库中存储内容的数据库连接。在六边形中,你将要测试的界面,可能是由一个模拟。在 Haskell 术语中,应用程序的核心是纯代码,端口是 IO。关键是,您想在 IO 接口处引入您的“接缝”(例如模拟)。所以你可能不想担心拆分 g 。
但是如何在 Haskell 中引入用于测试的“接缝”?毕竟,没有依赖注入框架(也不应该有)。嗯,这个问题的首选答案是,就像在 Haskell 中一样,使用函数和参数化。例如,假设您有一个函数 foo 是根据函数 bar 定义的。您想改变条形,因此它在您的测试中是一个测试替身,而在其余时间则是常规值。只需让 bar 成为这样的参数:
Module Foo
foo bar = ... bar ...
Module Test
foo = Foo.foo testBar
Module Real
foo = Foo.foo realBar
Run Code Online (Sandbox Code Playgroud)
你不需要完全那样做,但关键是参数化让你比你想象的更进一步。
好的,但是在 Haskell 中测试 IO 呢?我们如何“模拟”这些 IO 操作?一种方法是像在 JavaScript 中那样做:创建充满 IO 操作的数据结构(他们称它们为“对象”;-))并传递它们。另一种方法是不直接使用 IO 类型,而是通过两种 monadic 类型之一访问它 - real 和 test 类型都是定义要交换的操作的同一类型类的实例。或者您可以制作一个免费的 Monad(使用免费或可操作的软件包)并编写两个解释器 - 一个测试和一个真实的。
总而言之,测试纯代码非常简单,您尝试的任何方法几乎都能奏效。测试 IO 代码更难,这就是我们尽可能将其隔离的原因。
| 归档时间: |
|
| 查看次数: |
1505 次 |
| 最近记录: |