如何孤立地测试纯函数调用树?
How to test a tree of pure function calls in isolation?
在我们的 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
接口对这些函数实现的每种可能的边缘情况进行测试是笨拙的。
我们想到了一些解决方案,但不太喜欢,因为这两种方案都不像我们之前见过的模式。
解决方案一:
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
方法签名。
方案三:
接受foo
是一个复杂的复合运算的事实,作为一个整体进行测试。所有缺点都适用。
请提出其他可能性。我想这是函数式编程社区必须以某种方式解决的问题。
您可能不需要您考虑过的任何解决方案。函数式编程和命令式编程之间的区别之一是函数式风格应该产生更容易推理的代码。不仅仅是在精神上 "playing compiler" 和模拟一组给定的输入会发生什么,而是在更多的数学意义上推理你的代码。
例如,单元测试的目标是测试 "everything that can break." 看你发布的第一个代码片段,我们可以推理这个功能并问, "How could this function break?" 这是一个足够简单的功能,我们根本不需要玩编译器。我们可以说,如果 process()
函数未能为给定的一组输入 return 一个正确的值,即如果它 return 得到一个无效的结果或者它抛出一个例外。这反过来意味着我们还需要测试 extractBar()
和 extractBaz()
return 是否正确的结果,以便将正确的值传递给 process()
.
所以真的,你只需要测试 foo()
是否抛出意外异常,因为它所做的只是调用 process()
,你应该在它自己的集合中测试 process()
单元测试。 extractBar()
和 extractBaz()
也一样。如果这两个函数 return 在给定有效输入时正确的结果,它们将把正确的值传递给 process()
,如果 process()
在给定有效输入时产生正确的结果,那么 foo()
也将 return 纠正结果。
你可能会说,"What about the arguments? What if it extracts the wrong value from the data
structure?"但这真的能破吗?如果我们看一下这个函数,它使用核心 JS 点符号来访问对象的属性。我们不会在应用程序的单元测试中测试语言本身的核心功能。我们可以只看代码,因为它基于硬编码对象 属性 访问提取值,然后继续我们的其他测试。
这并不是说你可以扔掉你的单元测试,但是很多有经验的函数式程序员发现他们需要的测试要少得多,因为你只需要测试会破坏的东西,而功能性的编程减少了易碎物品的数量,因此您可以将测试重点放在真正有风险的部分上。
顺便说一句,如果您正在处理复杂的数据,并且您担心即使使用 FP 也可能很难推断出所有可能的排列,您可能需要研究生成测试.我想有一些 JS 库可以做到这一点。
在我们的 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
接口对这些函数实现的每种可能的边缘情况进行测试是笨拙的。
我们想到了一些解决方案,但不太喜欢,因为这两种方案都不像我们之前见过的模式。
解决方案一:
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
方法签名。
方案三:
接受foo
是一个复杂的复合运算的事实,作为一个整体进行测试。所有缺点都适用。
请提出其他可能性。我想这是函数式编程社区必须以某种方式解决的问题。
您可能不需要您考虑过的任何解决方案。函数式编程和命令式编程之间的区别之一是函数式风格应该产生更容易推理的代码。不仅仅是在精神上 "playing compiler" 和模拟一组给定的输入会发生什么,而是在更多的数学意义上推理你的代码。
例如,单元测试的目标是测试 "everything that can break." 看你发布的第一个代码片段,我们可以推理这个功能并问, "How could this function break?" 这是一个足够简单的功能,我们根本不需要玩编译器。我们可以说,如果 process()
函数未能为给定的一组输入 return 一个正确的值,即如果它 return 得到一个无效的结果或者它抛出一个例外。这反过来意味着我们还需要测试 extractBar()
和 extractBaz()
return 是否正确的结果,以便将正确的值传递给 process()
.
所以真的,你只需要测试 foo()
是否抛出意外异常,因为它所做的只是调用 process()
,你应该在它自己的集合中测试 process()
单元测试。 extractBar()
和 extractBaz()
也一样。如果这两个函数 return 在给定有效输入时正确的结果,它们将把正确的值传递给 process()
,如果 process()
在给定有效输入时产生正确的结果,那么 foo()
也将 return 纠正结果。
你可能会说,"What about the arguments? What if it extracts the wrong value from the data
structure?"但这真的能破吗?如果我们看一下这个函数,它使用核心 JS 点符号来访问对象的属性。我们不会在应用程序的单元测试中测试语言本身的核心功能。我们可以只看代码,因为它基于硬编码对象 属性 访问提取值,然后继续我们的其他测试。
这并不是说你可以扔掉你的单元测试,但是很多有经验的函数式程序员发现他们需要的测试要少得多,因为你只需要测试会破坏的东西,而功能性的编程减少了易碎物品的数量,因此您可以将测试重点放在真正有风险的部分上。
顺便说一句,如果您正在处理复杂的数据,并且您担心即使使用 FP 也可能很难推断出所有可能的排列,您可能需要研究生成测试.我想有一些 JS 库可以做到这一点。