I have been writing tests for a lot of long if/else trees recently, and I'm finding it a little discouraging. I want to speak in concrete terms, so consider the following example (I'll write in Ruby syntax but the example is quite general):
A user
can either be an admin?
or not (boolean method). A file
can either be public?
or not. An admin can open any file, and a public file can be opened by any user. A non-admin user cannot open a non-public file. To define whether a user can open a file, we make the following checks:
if user.admin? if file.public?
true true
elsif file.public? elsif user.admin?
true true
else else
false false
end end
I'll call the left implementation "admin-first", and the right one "file-first". Clearly, both implementations return the same values for every possibility.
I want to write a test for this block of code, which tests the behaviour and not the implementation. In particular, there are three test cases I care about:
- Given an admin user, and any file whatsoever, the block returns
true
. - Given any user whatsoever, and a public file, the block returns
true
. - Given a non-admin, non-public file, the block returns
false
.
My problem is that I cannot see a way to actually write tests which mean the above conditions.
One option ("maximum correctness") is to test all four options of true/false. Of course, this is a toy example and the cases I've been looking at have upwards of 15 conditions which can (in principle!) vary. Even with only four conditions, which is relatively common, this approach is impractical. It's also very ugly, especially in the extreme case: Why do I have to write more tests than there are distinct behaviours (in some cases, many many more)?
A similar option ("randomness") is choosing inputs randomly, but random tests are generally discouraged, and I believe the arguments against it.
A pragmatic approach when faced with the "admin-first" implementation is to test the following three cases instead:
- Provide an admin user (any file will do, because the tree will short-circuit).
- Provide a non-admin user with a public file.
- Provide a non-admin user with a non-public file.
I dislike this for several reasons. First, the behaviour case of "any user with a public file" is not checked. That's an aesthetic point really, but it also makes the tests confusing ("why is this property set? is it relevant?"). Second, the test will fail if I were to rearrange (preserving behaviour!) into the "file-first" implementation, so you're forced to actually write the tests to go along with the code, or try out every possiblity as before. As you go down the cases, the number of irrelevant bits of data you need to specify only gets bigger.
The tests can be made to pass even after reordering if we change the first check to specify that the file is non-public, rather than unspecifed. Again, I'm not satisfied:
- Now two of the three test cases fail to test the behaviour.
- In the case of more branches, you need to specify every option of data at every level of the tree, which is just plain awful.
- It is "intuitively obvious" that the overlap case (admin and public) does not need to be tested if the other two code paths work correctly. But that's only true if you know what the code looks like! In more complex cases, this inference is less clear and more confusing for a test-reader.
For the case of very many branches, you might say something like "refactor it to hide some of those booleans inside of other methods", but unfortunately I am writing these tests precisely because I want to refactor this code. The tests have to come first.
The two things I thought of first, namely using guard clauses or chains of &&
or ||
, have the same problem, because of short-circuiting.
So, my question: Is there any way to test code resembling this, or to restructure code that looks like this, in such a way that we can really test behaviour and not implementation?
nil
and that might do something sensible. But it's not intended behaviour.