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

时间:2021-05-27 14:29:47

In our team of JavaScript devs we have embraced redux/react style of writing pure functional code. However, we do seem to have trouble unit testing our code. Consider the following example:

在我们的JavaScript开发团队中,我们采用了redux / react风格编写纯函数代码。但是,我们似乎无法对我们的代码进行单元测试。请考虑以下示例:

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

This function call depends on calls to process, extractBar and extractBaz, each of which can call other functions. Together, they might require a non-trivial mock for data parameter to be constructed for testing.

这个函数调用依赖于对process,extractBar和extractBaz的调用,每个调用都可以调用其他函数。总之,他们可能需要构建用于测试的数据参数的非平凡模拟。

Should we accept the necessity of crafting such a mock object and actually do so in tests, we quickly find we have test cases that are hard to read and maintain. Furthermore, it very likely leads to testing the same thing over and over, as unit tests for process, extractBar and extractBaz should probably also be written. Testing for each possible edge case implemented by these functions via to foo interface is unwieldy.

我们是否应该接受制作这样一个模拟对象的必要性并且实际上在测试中这样做,我们很快发现我们有难以阅读和维护的测试用例。此外,它很可能导致一遍又一遍地测试相同的东西,因为也可能会编写process,extractBar和extractBaz的单元测试。通过foo接口测试由这些函数实现的每个可能的边缘情况是不实用的。


We have a few solutions in mind, but don't really like any, as neither seems like a pattern we have previously seen.

我们有一些解决方案,但并不是真的喜欢任何解决方案,因为它们似乎都不像我们之前看到过的模式。

Solution 1:

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

Solution 2:

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

Solution 2 pollutes foo method signature very quickly as the number of dependent function calls rises.

当依赖函数调用的数量增加时,解决方案2会非常快速地污染foo方法签名。

Solution 3:

Just accept the fact that foo is a complicated compound operation and test it as a whole. All the drawbacks apply.

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


Please, suggest other possibilities. I imagine this is a problem that the functional programming community must have solved in one way or another.

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

1 个解决方案

#1


7  

You probably don't need any of the solutions you've considered. One of the differences between functional programming and imperative programming is that the functional style should produce code that is easier to reason about. Not just in the sense of mentally "playing compiler" and simulating what would happen to a given set of inputs, but reasoning about your code in more of a mathematical sense.

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

For example, the goal of unit testing is to test "everything that can break." Looking at the first code snippet you posted, we can reason about the function and ask, "How could this function break?" It's a simple enough function that we don't need to play compiler at all. We can just say that the function would break if the process() function failed to return a correct value for a given set of inputs, i.e. if it returned an invalid result or if it threw an exception. That in turn implies that we also need to test whether extractBar() and extractBaz() return correct results, in order to pass the correct values to process().

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

So really, you only need to test whether foo() throws unexpected exceptions, because all it does is call process(), and you should be testing process() in its own set of unit tests. Same thing with extractBar() and extractBaz(). If these two functions return correct results when given valid inputs, they're going to pass correct values to process(), and if process() produces correct results when given valid inputs, then foo() will also return correct results.

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

You might say, "What about the arguments? What if it extracts the wrong value from the data structure?" But can that really break? If we look at the function, it's using core JS dot notation to access properties on an object. We don't test core functionality of the language itself in our unit tests for our application. We can just look at the code, reason that it's extracting the values based on hard-coded object property access, and proceed with our other tests.

你可能会说,“参数怎么样?如果它从数据结构中提取错误值会怎么样?”但真的可以打破吗?如果我们查看函数,它使用核心JS点表示法来访问对象的属性。在我们的应用程序的单元测试中,我们不测试语言本身的核心功能。我们可以查看代码,原因是它基于硬编码对象属性访问提取值,并继续我们的其他测试。

This is not to say that you can just throw away your unit tests, but a lot of experienced functional programmers find that they need a lot fewer tests, because you only need to test the things that can break, and functional programming reduces the number of breakable things so you can focus your tests on the parts that really are at risk.

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

And by the way, if you're working with complex data, and you're concerned that it might be difficult, even with FP, to reason out all the possible permutations, you might want to look into generative testing. I think there are a few JS libraries out there for that.

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

#1


7  

You probably don't need any of the solutions you've considered. One of the differences between functional programming and imperative programming is that the functional style should produce code that is easier to reason about. Not just in the sense of mentally "playing compiler" and simulating what would happen to a given set of inputs, but reasoning about your code in more of a mathematical sense.

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

For example, the goal of unit testing is to test "everything that can break." Looking at the first code snippet you posted, we can reason about the function and ask, "How could this function break?" It's a simple enough function that we don't need to play compiler at all. We can just say that the function would break if the process() function failed to return a correct value for a given set of inputs, i.e. if it returned an invalid result or if it threw an exception. That in turn implies that we also need to test whether extractBar() and extractBaz() return correct results, in order to pass the correct values to process().

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

So really, you only need to test whether foo() throws unexpected exceptions, because all it does is call process(), and you should be testing process() in its own set of unit tests. Same thing with extractBar() and extractBaz(). If these two functions return correct results when given valid inputs, they're going to pass correct values to process(), and if process() produces correct results when given valid inputs, then foo() will also return correct results.

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

You might say, "What about the arguments? What if it extracts the wrong value from the data structure?" But can that really break? If we look at the function, it's using core JS dot notation to access properties on an object. We don't test core functionality of the language itself in our unit tests for our application. We can just look at the code, reason that it's extracting the values based on hard-coded object property access, and proceed with our other tests.

你可能会说,“参数怎么样?如果它从数据结构中提取错误值会怎么样?”但真的可以打破吗?如果我们查看函数,它使用核心JS点表示法来访问对象的属性。在我们的应用程序的单元测试中,我们不测试语言本身的核心功能。我们可以查看代码,原因是它基于硬编码对象属性访问提取值,并继续我们的其他测试。

This is not to say that you can just throw away your unit tests, but a lot of experienced functional programmers find that they need a lot fewer tests, because you only need to test the things that can break, and functional programming reduces the number of breakable things so you can focus your tests on the parts that really are at risk.

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

And by the way, if you're working with complex data, and you're concerned that it might be difficult, even with FP, to reason out all the possible permutations, you might want to look into generative testing. I think there are a few JS libraries out there for that.

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