如何独立地测试纯函数调用的树? [英] How to test a tree of pure function calls in isolation?

查看:124
本文介绍了如何独立地测试纯函数调用的树?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在我们的JavaScript开发团队中,我们采用了编写纯功能代码的减少/反应风格。但是,我们似乎无法单元测试我们的代码。考虑下面的例子:

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

$ / code>

这个函数调用依赖于调用 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 是一个复杂的复合操作并将其作为一个整体进行测试的事实。所有缺点适用。






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

解决方案

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

例如,单元测试的目标是测试可能破坏的所有东西。看看你发布的第一个代码片段,我们可以推理这个函数,并问这个函数怎么会中断?这是一个简单的函数,我们根本不需要玩编译器。如果 process()函数未能为给定的一组输入返回正确的值,也就是说它返回了无效结果或if它抛出了一个例外。这又意味着我们还需要测试是否按照顺序返回正确的结果 extractBar() extractBaz()将正确的值传递给 process()



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



<你可能会说:那么这个论点呢?如果它从 data 结构中提取了错误的值呢?但那真的能打破吗?如果我们看看这个函数,它使用核心JS点符号来访问对象的属性。我们不在我们的应用程序的单元测试中测试语言本身的核心功能。我们可以看看代码,理由是它基于硬编码的对象属性访问提取值,并继续进行其他测试。

这并不是说你可以扔掉你的单元测试,但是很多经验丰富的函数程序员发现他们需要少得多的测试,因为你只需要测试可能中断的东西,而函数式编程减少了易碎事物的数量,所以你可以如果你正在处理复杂的数据,并且担心它可能会成为问题很难,即使是FP,也可以推断出所有可能的排列方式,但您可能需要研究生成性测试。我认为那里有几个JS库。


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:

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.

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.


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.

Solution 3:

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


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

解决方案

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().

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.

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.

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.

这篇关于如何独立地测试纯函数调用的树?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆