Javascript 如何单独测试纯函数调用树?

Javascript 如何单独测试纯函数调用树?,javascript,unit-testing,functional-programming,pure-function,Javascript,Unit Testing,Functional Programming,Pure Function,在我们的JavaScript开发团队中,我们采用了编写纯功能代码的redux/react风格。然而,我们似乎在单元测试代码时遇到了麻烦。考虑下面的例子: function foo(data) { return process({ value: extractBar(data.prop1), otherValue: extractBaz(data.prop2.someOtherProp) }); } 此函数调用取决于对进程、提取栏和提取栏的调用,每

在我们的JavaScript开发团队中,我们采用了编写纯功能代码的redux/react风格。然而,我们似乎在单元测试代码时遇到了麻烦。考虑下面的例子:

function foo(data) {
    return process({
        value: extractBar(data.prop1),
        otherValue: extractBaz(data.prop2.someOtherProp)
    });
}
此函数调用取决于对
进程
提取栏
提取栏
的调用,每个调用都可以调用其他函数。总之,它们可能需要为测试构建一个非平凡的模拟
数据
参数

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


我们心中有一些解决方案,但实际上并不喜欢任何解决方案,因为这两种解决方案似乎都不是我们以前见过的模式

解决方案1:

function foo(data, deps = defaultDeps) {
    return deps.process({
        value: deps.extractBar(data.prop1),
        otherValue: deps.extractBaz(data.prop2.someOtherProp)
    });
}
function foo(
    data, 
    processImpl = process, 
    extractBarImpl = extractBar, 
    extractBazImpl = extractBaz
) {
    return process({
        value: extractBar(data.prop1),
        otherValue: extractBaz(data.prop2.someOtherProp)
    });
}
解决方案2:

function foo(data, deps = defaultDeps) {
    return deps.process({
        value: deps.extractBar(data.prop1),
        otherValue: deps.extractBaz(data.prop2.someOtherProp)
    });
}
function foo(
    data, 
    processImpl = process, 
    extractBarImpl = extractBar, 
    extractBazImpl = extractBaz
) {
    return process({
        value: extractBar(data.prop1),
        otherValue: extractBaz(data.prop2.someOtherProp)
    });
}
随着依赖函数调用数量的增加,解决方案2会很快污染
foo
方法签名

解决方案3:

function foo(data, deps = defaultDeps) {
    return deps.process({
        value: deps.extractBar(data.prop1),
        otherValue: deps.extractBaz(data.prop2.someOtherProp)
    });
}
function foo(
    data, 
    processImpl = process, 
    extractBarImpl = extractBar, 
    extractBazImpl = extractBaz
) {
    return process({
        value: extractBar(data.prop1),
        otherValue: extractBaz(data.prop2.someOtherProp)
    });
}
只需接受
foo
是一个复杂的复合操作这一事实,并将其作为一个整体进行测试。所有的缺点都适用



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

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

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

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

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

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

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