如何隔离测试纯函数调用树



在我们的JavaScript开发团队中,我们接受了编写纯函数式代码的redux/act风格。但是,我们似乎确实在对代码进行单元测试时遇到了麻烦。请考虑以下示例:

function foo(data) {
    return process({
        value: extractBar(data.prop1),
        otherValue: extractBaz(data.prop2.someOtherProp)
    });
}

这个函数调用依赖于对processextractBarextractBaz的调用,每个函数都可以调用其他函数。总之,它们可能需要一个非平凡的模拟来构造data参数进行测试。

如果我们接受制作这样一个模拟对象的必要性,并在测试中实际这样做,我们很快就会发现我们有难以阅读和维护的测试用例。此外,它很可能导致一遍又一遍地测试同样的东西,因为processextractBarextractBaz的单元测试可能也应该编写。测试这些功能通过 to 接口实现的每个可能的边缘情况foo很笨拙。


我们有一些解决方案,

但并不真正喜欢任何解决方案,因为两者都不像我们以前见过的模式。

解决方案 1:

function foo(data, deps = defaultDeps) {
    return deps.process({
        value: deps.extractBar(data.prop1),
        otherValue: deps.extractBaz(data.prop2.someOtherProp)
    });
}

解决方案 2:

function foo(
    data, 
    processImpl = process, 
    extractBarImpl = extractBar, 
    extractBazImpl = extractBaz
) {
    return process({
        value: extractBar(data.prop1),
        otherValue: extractBaz(data.prop2.someOtherProp)
    });
}

随着依赖函数调用数量的增加,解决方案 2 会很快污染foo方法签名。

解决方案 3:

只需接受foo是一个复杂的复合操作的事实,并对其进行整体测试即可。所有缺点都适用。


请提出其他可能性。我想这是函数式编程社区必须以某种方式解决的问题。

您可能不需要任何您考虑过的解决方案。函数式编程和命令式编程之间的区别之一是,函数式风格应该产生更容易推理的代码。不仅仅是在心理上"玩编译器"并模拟给定的一组输入会发生什么,而是在更多的数学意义上对代码进行推理。

例如,单元测试的目标是测试"所有可能中断的东西"。查看您发布的第一个代码片段,我们可以对函数进行推理并问:"这个函数怎么会坏?这是一个非常简单的函数,我们根本不需要玩编译器。我们可以说,如果process()函数未能为给定的一组输入返回正确的值,即如果它返回无效结果或抛出异常,则该函数将中断。这反过来意味着我们还需要测试extractBar()extractBaz()是否返回正确的结果,以便将正确的值传递给process()

所以实际上,你只需要测试foo()是否抛出意外的异常,因为它所做的只是调用process(),你应该在它自己的一组单元测试中测试process()extractBar()extractBaz()也是如此.如果这两个函数在给定有效输入时返回正确的结果,它们会将正确的值传递给process(),如果process()在给定有效输入时产生正确的结果,那么foo()也将返回正确的结果。

你可能会说,"争论呢?如果它从data结构中提取了错误的值怎么办?但这真的能打破吗?如果我们看一下函数,它使用核心 JS 点表示法来访问对象的属性。我们不会在应用程序的单元测试中测试语言本身的核心功能。我们可以只查看代码,推理它基于硬编码的对象属性访问提取值,然后继续进行其他测试。

这并不是说你可以扔掉你的单元测试,但是很多有经验的函数式程序员发现他们需要的测试要少得多,因为你只需要测试可以破坏的东西,而函数式编程减少了可破坏的东西的数量,所以你可以把测试集中在真正有风险的部分上。

顺便说一下,如果你正在处理复杂的数据,并且你担心即使使用FP也可能很难推理出所有可能的排列,你可能想研究生成测试。我认为有一些JS库可以做到这一点。

相关内容

  • 没有找到相关文章

最新更新