在我们的JavaScript开发团队中,我们采用了redux / react风格编写纯函数代码。但是,我们似乎无法对我们的代码进行单元测试。请考虑以下示例:
function foo(data) {
return process({
value: extractBar(data.prop1),
otherValue: extractBaz(data.prop2.someOtherProp)
});
}
此函数调用取决于对process
,extractBar
和extractBaz
的调用,每个调用都可以调用其他函数。总之,他们可能需要构建data
参数的非平凡模拟来进行测试。
我们是否应该接受制作这样一个模拟对象的必要性,并且在测试中实际这样做,我们很快发现我们有难以阅读和维护的测试用例。此外,由于process
,extractBar
和extractBaz
的单元测试也应该被编写,因此很可能会反复测试相同的内容。通过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
是一个复杂的复合操作并将其作为一个整体进行测试的事实。所有缺点都适用。
请提出其他可能性。我想这是一个功能编程社区必须以某种方式解决的问题。
答案 0 :(得分:7)
您可能不需要您考虑过的任何解决方案。函数式编程和命令式编程之间的区别之一是函数式应该生成更容易推理的代码。不只是在精神上和#34;玩编译器"并模拟一组给定输入会发生什么,但在更多数学意义上推理你的代码。
例如,单元测试的目标是测试所有可能破坏的东西。"看看你发布的第一个代码片段,我们可以推断出这个功能,然后问一下,"这个功能怎么会破坏?"这是一个足够简单的功能,我们根本不需要玩编译器。我们可以说,如果process()
函数未能为给定的一组输入返回正确的值,即如果它返回了无效结果或者它引发了异常,那么函数就会中断。这反过来意味着我们还需要测试extractBar()
和extractBaz()
是否返回正确的结果,以便将正确的值传递给process()
。
实际上,您只需要测试foo()
是否会抛出意外异常,因为它所做的只是调用process()
,您应该在其自己的单元测试集中测试process()
。与extractBar()
和extractBaz()
相同。如果这两个函数在给定有效输入时返回正确的结果,则它们会将正确的值传递给process()
,如果process()
在给定有效输入时产生正确的结果,则foo()
也将返回正确的结果。
你可能会说,"论点怎么样?如果它从data
结构中提取错误值会怎么样?"但真的可以打破吗?如果我们查看函数,它会使用核心JS点表示法来访问对象的属性。在我们的应用程序的单元测试中,我们不测试语言本身的核心功能。我们可以查看代码,理由是它根据硬编码对象属性访问提取值,然后继续我们的其他测试。
这并不是说你可以放弃你的单元测试,但是许多经验丰富的函数式程序员发现他们需要的测试要少得多,因为你只需要测试可能会破坏的东西,并且函数式编程会减少可破坏的东西的数量,这样你就可以将你的测试集中在真正有风险的部分上。
顺便说一下,如果您正在使用复杂的数据,并且您担心即使使用FP也可能难以推断所有可能的排列,您可能需要考虑一下生成测试。我认为那里有一些JS库。