On tests, mocking, and pragmatism
I spent this week futzing with mocked unit tests. Futzing unsuccessfully I might add.As I progressed, or rather didn't progress, I came to some realizations about testing and mocking. This runs against the grain of the orthodoxy on the subject. The normal, accepted, answer is that every function and every path needs tests. Some things are hard to test so then you bring in a mocking framework to allow testing of external dependencies that are hard to test otherwise.This is well and good. To an extent.Let me take a step back to the notion of test driven development -- TDD. The guiding principal there is that the test should be written first, then the code to pass the test. In the perfect case this is pure black-box testing: for a given input the output needs to match the expected result.As you go down the path of mocking and testing, you get to an interesting juncture as the box slowly grows more transparent.Say, for instance, you have this code:
foo(int in) {a = doA(in)b = doB(a)c = doC(b)return c}doA(in) {x = callService(in)y = transform(x)z = y * 42;}
The "right" way would be to mock doA, doB, and doC and test that the right things get passed. This is complicated by the fact that you might not be able to mock these functions, but rather might need to mock what these functions do.Pretty soon the unit test for foo turns into an unrolled version of the foo function itself with everything specified. Everything is specified to make the test work. You might need to mock callService and transform as well as anything else.As this path is traversed, the box starts shifting from black to white, and right onto clear. The test is written to pass the function that was written, not the other way around. This continues as if code under test changes -- when the test breaks, the general reaction is to fix the test.This is actually the opposite of TDD. This, in the degenerate case, turns into code driven test creation.
- = -
My view is that if it's inconceivable to write the test before the function you shouldn't. Perhaps you should break the code up into smaller parts that can be tested. But I'm not nearly as enthused about testing to verify that the compiler can do its job.If the code reads nicely, I am less interested in testing the fact that the VM can call the functions.Now, keep in mind that I'm not opposed to tests. doA should have a test, maybe with mocks to boot. But the underlying implementation of foo should be kept hidden, even from the test. When the test needs to know how the inner works, and worse still the inner functions as well.Eventually it gets to the point where the code and the test are so intertwined that changing either breaks the other. At that point things have pretty much broke. When any code change causes the test to break, the test is nothing but a gating condition for the code to get checked in even though the expected behavior is for it to break.