如何在函数式编程中编写好的单元测试

Ste*_*tis 14 language-agnostic unit-testing functional-programming dependency-injection

我正在使用函数而不是类,我发现我无法分辨它依赖的另一个函数是应该单独进行单元测试的依赖还是不应该进行单元测试的内部实现细节.你怎么知道它是哪一个?

一点上下文:我正在写一个非常简单的Lisp解释器,它有一个eval()函数.它将承担很多责任,实际上太多,例如以不同于列表的方式评估符号(其他一切评估自己).在评估符号时,它有自己的复杂工作流程(环境查找),在评估列表时,它甚至更复杂,因为列表可以是宏,函数或特殊形式,每个都有自己复杂的工作流程和一套责任.

我不知道我的eval_symbol()eval_list()函数是否应该被认为是内部实现细节eval()应该通过eval()自己的单元测试来测试,或者它们本身的真正依赖性应该是独立于eval()单元测试的单元测试.

WRe*_*ach 17

"单元测试"概念的一个重要动机是控制所需测试用例的组合爆炸.让我们来看看的例子eval,eval_symboleval_list.

在这种情况下eval_symbol,我们将要测试符号绑定的偶然事件:

  • 丢失(即符号未绑定)

  • 在全球环境中

  • 直接在当前环境中

  • 从包含环境继承而来

  • 影响另一个绑定

  • ... 等等

在这种情况下eval_list,我们将要测试(除其他外)当列表的函数位置包含符号时​​发生的情况:

  • 没有功能或宏绑定

  • 功能绑定

  • 宏绑定

eval_listeval_symbol只要需要符号的绑定就会调用(假设是LISP-1).假设有S个测试用例eval_symbolL符号相关的测试用例eval_list.如果我们分别测试这些函数中的每一个,我们就可以使用大致与S + L符号相关的测试用例.但是,如果我们希望将其eval_list视为一个黑盒子并在没有任何eval_symbol内部使用知识的情况下对其进行详尽测试,那么我们将面临与S x L符号相关的测试用例(例如全局函数绑定,全局宏绑定,本地函数绑定) ,本地宏绑定,继承函数绑定,继承宏绑定等).这是更多的情况. eval更糟糕的是:作为一个黑匣子,组合的数量可能会变得非常大 - 因此称为组合爆炸.

因此,我们面临着理论纯度与实际实用性的选择.毫无疑问,只运行"公共API"(在这种情况下eval)的一组全面的测试用例给出了最大的信心,即没有错误.毕竟,通过运用所有可能的组合,我们可能会发现微妙的集成错误.然而,这种组合的数量可能非常大,以至于排除了这种测试.更不用说程序员可能会犯错误(或疯狂)审查大量仅以微妙方式不同的测试用例.通过对较小的内部组件进行单元测试,可以大大减少所需测试用例的数量,同时仍然保持对结果的高度信任 - 这是一种实用的解决方案.

因此,我认为确定单元测试粒度的指导原则是:如果测试用例的数量非常大,那么就开始寻找更小的单元进行测试.

在手头的情况下,我绝对提倡测试eval,eval-list并且eval-symbol正好因为组合爆炸而作为单独的单元.在编写测试时eval-list,您可以依靠eval-symbol坚如磐石,将注意力集中eval-list在自身增加的功能上.境内有可能其他的可测试单位eval-list为好,如eval-function,eval-macro,eval-lambda,eval-arglist等.


Joh*_*ler 6

我的建议很简单:"从某个地方开始!"

  • 如果你看到一些def(或deffun)的名字,看起来它可能很脆弱,那么,你可能想测试它,不是吗?
  • 如果您在尝试弄清楚客户端代码如何与其他代码单元接口时遇到一些麻烦,那么您可能希望在某处编写一些测试,以便您创建如何正确使用该函数的示例.
  • 如果某些函数对数据值似乎很敏感,那么您可能希望编写一些测试,不仅可以验证它是否可以正确处理任何合理的输入,还可以专门处理边界条件和奇数或异常数据输入.
  • 无论什么似乎容易出错都应该进行测试.
  • 无论什么似乎不清楚都应该进行测试.
  • 无论什么似乎复杂应该进行测试.
  • 无论什么似乎重要应该进行测试.

之后,您可以将覆盖率提高到100%.但是你会发现,你的单元测试编码的前20%可能得到80%的实际结果(倒置的"关键少数法则").

那么,回顾一下我谦虚方法的要点,"从某个地方开始!"

关于问题的最后一部分,我建议您考虑任何可能的递归或者您或后续开发人员将来可能创建的"客户端"函数的任何其他可能的重用,这些函数也将调用eval_symbol()或eval_list().

关于递归,函数式编程风格使用了很多,并且很难做到正确,特别是对于我们这些来自程序或面向对象编程的人来说,递归似乎很少遇到.获得正确递归的最佳方法是使用单元测试精确地定位任何递归功能,以确保验证所有可能的递归用例.

关于重用,如果你的函数可能被eval()函数单独使用以外的任何东西调用,它们应该被视为真正的依赖,值得进行独立的单元测试.

作为最后的提示,术语"单元"在单元测试领域中具有技术定义,即"可以单独测试的最小代码软件"..这是一个非常古老的基本定义,可能会很快为您澄清您的情况.