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.
In computer programming, a pure function is a function that has the following properties: the function return values are identical for identical arguments (no variation with local static variables, non-local variables, mutable reference arguments or input streams), and.
Yes you can do the same to Math. random() if you memoize it making every call to pureRandom return exactly the same number. And this would make pureRandom pure because it always returns the same result. I personally would not call it a "random" function.
A function must pass two tests to be considered “pure”: Same inputs always return same outputs. No side-effects.
Your function is pure if it does not contain any external code. Otherwise, it is impure if it includes one or more side effects.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With