A unit test sees the code it tests as a black box. Its only concern is to verifies functionality. It really doesn’t care about how the functionality is realized. It actually has a much tighter coupling to the requirements, because if they don’t match, your out of luck.
We test functionality, not implementation details
When we write a unit test, we are testing functionality. I’ve said it many times before, but it is important to understand. We do not test code. The test doesn’t care about how the functionality is implemented. But what do I mean by that?
This is my definition. When we test the code, the test-code is tightly coupled with the production code. It knows that there is an if, that we set a specific variable and so on.
We really don’t want that because that would make the tests very brittle. If we change the code, we have to change the tests too. That means that the test no longer can verify that our changes were correctly, because the tests are a part of the change itself.
An analogy of tightly coupled code
We are going to filling one litre of water into a jug. The requirement in this case is one litre.
Now, the jug has this shape, and the one litre mark is five centimetres up. We can test that if the water level is at this mark when we have stopped poring the water, we have successfully filled it with the right amount of water. And then, we refactor. We switch the jug to another one, that is much narrower. That means that the one litre mark now is at ten centimetres. So, we have to change the test too.
This is an example of a test that is tightly coupled to the “source”.
First of all, it makes us work twice as much every time we rewrite the code. Quite the opposite to gaining speed when the tests are done.
Second, we’re not testing the same thing as before, so we can’t say with confidence that nothing has changed. We need the stability of a test that doesn’t change to really know that the behaviour also hasn’t changed. If we alter the test, we’re actually altering the requirement too. And that nullifies the use of the test. It becomes extra code that doesn’t give us anything but more work.
Third, we can’t trust the tests because when we change them, we might introduce bugs. The test has to be stable and maybe adding bugs is certainly not that.
And finally, fourth. The requirement is not reflected in the test. We want one litre, but we test five centimetres. Try using that as a documentation. It is just confusing.
Loosely coupled test
But what if we test is to measure the volume of the water instead of the level? For one, it is testing the actual requirement. Which means that we can use the test to understand the requirement. And since the test doesn’t care about which kind of jug we use, we test the litre and that is the same every time, we are free to do whatever we want with the water container. For all I care, use a sponge to soak up the water, it doesn’t matter as long as it is one litre we soak up.
This is why we want tests that doesn’t test code!
We want an independent mechanism that, no matter how much we change the code, just sits there, and says jay or nay. Something that has so hight trust that we can rely on its judgement. And to achieve that, we have to construct the tests to be as independent as possible from the thing it is testing.
The test becomes the requirement
We start the process with a set of requirements that describes the functionality, that describes what we want the code to do, what the result of the code should be. The requirements are isolated form the code. They don’t mention, with a single word, what the implementation should look like. It only describes what we put in, and what we get out. The requirements are totally unaware of the algorithms, the code details.
And this is what we actually write tests for. This is what the unit tests test. Unit tests validate requirements.
Let me say that again. Unit tests validate requirements!
I submit to you; unit tests and requirements are just two different ways to write the specifications of the system. One is more like prose, written by people that are good with words. The other is written by people that know code. But they both do the same exact thing. They define the input and the output.
I told you that the unit tests are documentation.
This is also why TDD is so great. You have the requirements in text. Then you translate them into the other form, the unit test-code. When you have done that, you can verify them against each other. You actually have a way verify the unit-test code against the requirements. Make sure that the quality is there, that you are doing things correct. Before you have written one single line of code.
So, what about the code then?
Well, that’s just a technical detail. It really doesn’t matter how the functionality is implemented, as long as it performs according to the requirements/ tests. In fact, that is exactly how it’s supposed to be. Because, when the code and the tests are as disconnected as humanly possible, you can rewrite the code from scratch and still get an ok if you did it correct.
This is important from another aspect too, as I’ve also mentioned before. When you have this level of confidence in the validation, the barrier to change the code seizes to exist. You will not feel bad when you see code that needs refactoring. It will become a thing that you just do. The Scout Rule moves from just a good idea to something that you do without thinking. And that will in the long run give you higher productivity.