I usually write my code in a test driven style. I write tests as specifications and then my code. It's great and useful.
I always try to ignore implementation when testing and only test behaviour. I don't care how it gets done, just that it got done. I find this especially easy for functional programming.
Now here is the problem I have found. I have an app that is written in a functional style. All of the unit tests are nice, clean, behavioural tests. I only ever check output and don't do things like "did you call this function?".
At some point however, I start needing "glue" functions. I'll consider these functions that don't introduce a lot of functionality and largely just call my other existing functions. Perhaps chaining a bunch together or whatever feature it may be.
How do I test these glue functions?
I ask because I want to avoid two main things as much as possible:
- I don't want to test them by simply mocking what they do and seeing if specific functions were called.
- They have a desired output I want, but usually this output is just a series of outputs from other functions that are already tested. I don't want to repeat myself and just "re-test" those inner functions to see if my glue function called them.
Hopefully that makes sense. Here is an example (written in pseudo code):
func1 (x) => x + 1;
func2 (x) => x * 2;
glue (x) => [func1(x), func2(x)];
Here would be a simple way of testing these functions.
testFunc1 () => expect func1(2) == 3;
testFunc2 () => expect func1(2) == 4;
testGlue () => expect glue(2) == [3, 4];
So obviously, glue
has an expected and predictable behaviour I want to model. I know that in this example these tests might be ok. So consider instead that the outputs of func1
and func2
are not simple numbers but much more complicated objects.
In such a case, implementing the checks that glue
output the correct objects would be tedious AND it would be totally duplicated from the individual tests of func1
and func2
. This also leads into the next issue.
Instead consider:
testGlue () => expect glue(2) == [func1(2), func2(2)];
This certainly seems better. But I think it is still flawed. While this means I am not repeating my test code it now instead "tests that the code you wrote is the code you wrote" (as opposed to what the behaviour is). Again, in such a small example it's not an issue so pretend that within glue
a few variables are swapped around and yada yada is done so that to test it in this way would require my test to also set up the variables like such. Then we would be basically copying the code from the function to check if func1
and func2
were called with the correct variables leading to repetition and testing that "it's the code you wrote".
If a larger example is needed to showcase such results just let me know and I will get one. Hopefully there is some good discussion to be had here.
I anticipate someone to answer "don't use glue functions" and to that I preemptively ask, "what's the alternative method?".
EDIT:
So I am beginning to think that an alternate question that would also give me the answer I want is this.
Consider that the output of func1
and func2
is something too big to feasibly have as a hardcoded value in the test. Maybe it's an object or something.
Does writing my test of glue
as:
testGlue () => expect glue(2) == [func1(2), func2(2)];
No hold on, I must clarify something. Obviously the above test is absolutely stupid. It is just "the code I wrote is the code I wrote".
We MUST imagine that the function does more than this. In a real world scenario glue
would do some processing of x
before passing it around. The order of the array might matter. And however many other options. So maybe I'm checking that glue(2)[0] = func1(3)
instead (pretend there is further processing to it).
In such a case, is it still considered bad practice to use the output of a function as something to test against (even though that function is tested somewhere else)?
testFunc1 () => expect func1(2) == 3;
, you are explicitely coupling your test to the internal implementation of glue. A tests is supposed to be a stand-in for real client code, and client code is not supposed to know (in this version) that glue calls func1 or func2. The other thing is, glue is kind of doing at least two things - orchestrating the calls, and combining the return values. If combining the return values is not trivial, it can be a separate function. Otherwise a test like== [3, 4]
is fine.func1
andfunc2
will always return full and proper objects, then for testing your glue, you can relatively safely check a subset of properties that provides evidence that func1/func2 were called properly without checking every single property. That being said, I've always found it's best to have one full integration test to make sure tiny bugs don't slip through. So, one very detailed default test, then just tests to test the logic of your glue function itself.