Enums are simply finite types, with custom (hopefully meaningful) names. An enum might only have one value, like void
which contains only null
(some languages call this unit
, and use the name void
for an enum with no elements!). It may have two values, like bool
which has false
and true
. It may have three, like colourChannel
with red
, green
and blue
. And so on.
If two enums have the same number of values, then they're "isomorphic"; i.e. if we switch out all of the names systematically then we can use one in place of another and our program will not behave any differently. In particular, our tests will not behave any differently!
For example, result
containing win
/lose
/draw
is isomorphic to the above colourChannel
, since we can replace e.g. colourChannel
with result
, red
with win
, green
with lose
and blue
with draw
, and as long as we do so everywhere (producers and consumers, parsers and serialisers, database entries, log files, etc.) then there will be no change in our program. Any "colourChannel
tests" we wrote will still pass, even though there is no colourChannel
any more!
Also, if an enum contains more than one value, we can always rearrange those values to get a new enum with the same number of values. Since the number of values hasn't changed, the new arrangement is isomorphic to the old one, and hence we could switch out all the names and our tests would still pass (note that we can't just switch out the definition; we must still switch out all use sites as well).
What this means is that, as far as the machine is concerned, enums are "distinguishable names" and nothing else. The only thing we can do with an enum is to branch on whether two values are the same (e.g. red
/red
) or different (e.g. red
/blue
). So that's the only thing that a 'unit test' can do, e.g.
( red == red ) || throw TestFailure;
(green == green) || throw TestFailure;
( blue == blue ) || throw TestFailure;
( red != green) || throw TestFailure;
( red != blue ) || throw TestFailure;
...
As @jesm00 says, such a test is checking the language implementation rather than your program. These tests are never a good idea: even if you don't trust the language implementation, you should test it from the outside, since it can't be trusted to run the tests correctly!
So that's the theory; what about the practice? The main issue with this characterisation of enums is that 'real world' programs are rarely self-contained: we have legacy versions, remote/embedded deployments, historical data, backups, live databases, etc. so we can never really 'switch out' all occurrences of a name without missing some uses.
Yet such things are not the 'responsibility' of the enum itself: changing an enum might break communication with a remote system, but conversely we might fix such a problem by changing an enum!
In such scenarios, the enum is a red-herring: what if one system needs it to be this way, and another needs it to be that way? It can't be both, no matter how many tests we write! The real culprit here is the input/output interface, which should produce/consume well defined formats rather than "whatever integer the interpret picks". So the real solution is to test the i/o interfaces: with unit tests to check that it's parsing/printing the expected format, and with integration tests to check that the format is actually accepted by the other side.
We may still wonder whether the enum is being 'exercised thoroughly enough', but in this case the enum is again a red herring. What we're actually concerned about is the test suite itself. We can gain confidence here in a couple of ways:
- Code coverage can tell us if the variety of enum values coming from the test suite are enough to trigger the various branches in the code. If not, we can add tests which trigger the uncovered branches, or generate a wider variety of enums in the existing tests.
- Property checking can tell us if the variety of branches in the code is enough to handle the runtime possibilities. For example, if the code only handles
red
, and we only test with red
, then we have 100% coverage. A property checker will (try to) generate counterexamples to our assertions, such as generating the green
and blue
values we forgot to test.
- Mutation testing can tell us whether our assertions actually check the enum, rather than just following the branches and ignoring their differences.