7

在我们的 JavaScript 开发团队中,我们采用了编写纯函数式代码的 redux/react 风格。然而,我们似乎在对我们的代码进行单元测试时遇到了麻烦。考虑以下示例:

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

此函数调用取决于对和的调用process,其中每个函数都可以调用其他函数。总之,它们可能需要一个非平凡的模拟 参数来构建测试。extractBarextractBazdata

如果我们接受制作这样一个模拟对象的必要性并在测试中实际这样做,我们很快就会发现我们有难以阅读和维护的测试用例。此外,它很可能会导致反复测试相同的东西,就像对 , 的单元测试一样processextractBar并且extractBaz可能也应该编写。通过 tofoo接口对这些函数实现的每个可能的边缘情况进行测试是很麻烦的。


我们想到了一些解决方案,但并不真正喜欢任何解决方案,因为它们看起来都不像我们以前见过的模式。

解决方案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)
    });
}

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

解决方案3:

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


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

4

1 回答 1

7

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

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

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

你可能会说,“参数呢?如果它从data结构中提取了错误的值怎么办?” 但这真的可以打破吗?如果我们查看函数,它使用核心 JS 点符号来访问对象的属性。我们不会在应用程序的单元测试中测试语言本身的核心功能。我们可以只看代码,因为它是根据硬编码的对象属性访问来提取值的,然后继续我们的其他测试。

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

顺便说一句,如果您正在处理复杂的数据,并且您担心即使使用 FP 也可能难以推理出所有可能的排列,您可能需要研究生成测试。我认为那里有一些 JS 库。

于 2016-03-08T15:26:24.240 回答