TDD is hard, seriously hard. Perhaps that’s a bit of an overstatement, but for me, after years of “skill neglect”, it’s been a real struggle to pick up and get right.
The problem isn’t writing the tests, it’s writing code that’s testable. I’ve dabbled a bit with unit testing in the past but never really got the “unit” part of it. My tests would dive into a class that relied upon other classes and I would assert some value passed all the way through the call stack. In hindsight this is totally wrong (unless we’re talking functional testing) but at the time, my only concern was getting a passing test, which I was successfully achieving. It was testing, but not as I now know it.
So now I know better. Now I know that a unit test should only really concern itself with the class at hand. The results returned should be a direct product of the method called and should not have been meddled with by a third party class/library/what-have-you. Before really taking some time to learn the subject of testing, I would have looked at my testee, thought “Well this class relies upon this other class here, what can I do but let the call happen.” and as such my unit tests turned into functional tests. But now I know I can substitute those called classes out for fakes, dummies, stubs or mocks.
I started out by writing tests for some existing legacy code (TDD arse about face really) and really struggled with working out how to use my mocks as callees for my target testee. I was instantiating classes inside methods and then making calls out to it. There was no way of substituting the concrete class out for my mock. But now I know I can substitute classes using dependency injection.
The nice thing about mocks and dependency injection, is that you’re not just able to verify the result of your testee, you can also verify it’s interaction with the mock. If your testee is only supposed to call getAmazingValue() once, you can fail if the method is called more times than that, or perhaps even called with the wrong parameters. I would have taken it on almost blind faith that the interactions I had been creating were correct. But now I know I can record, replay and verify these interactions. *
The hardest part for me was writing code I could test. TDD forced me to think about the design of my classes all the time, every time. When doing TDD I start with the test. I think about how I would like the call to work and then I code up something just to make the test pass. Then I’ll think about how my class relies upon a call out to some other class. So I’ll create a small mock for the testee, go about injecting it to get the test working again and then make sure the interaction between the testee and mock happened exactly as it should of. TDD is a beautiful thing, but it has its thorns. Just like everything in computing, on the surface the concept is deceptively simple. But when you really start to look into it and try and do it properly, what seemed like just a piece of floating ice is really the tip of a huge iceberg. The pain of doing it right is well worth it with the reward of less bugs and easier changes. It’s a hard concept to keep going and feel like it’s worth it, as the benefits sometimes might not be seen for some time. It’s only in that moment of changing something that you see straight away that it’s broken other pieces of implementation, do you really feel glad that you made the effort.
- While mocks allow for the testing of the interaction between the testee and the mock, this doesn’t always mean the relationship is correct. If I mock a class and expect a call to getAss() but the real concrete class expects a call to getArse(), then the whole thing falls apart when run in the wild. This might not be such a problem when the mock is derived from a common interface, but in dynamic languages, there is that margin for error.