Bob Martin made a very insightful and loaded blog post today. Highly recommend!
I definitely have a lot to chime in about this. I have spent the past 4~ months basically going to Uncle Bob University - watching his talked on YouTube, reading and re-reading his many insightful blog posts, buying some of his cleancoders.com videos, and his Clean Code and Architecture in C# book.
When I started, my head was spinning with a lot of information - a lot of it conflicting. In Comp Sci classes, they teach you to have a unit test for every. single. method. I started taking this approach where my tests were one to one with my production code. It got to a point where I psychologically became conservative to make changes to avoid breaking lots of tests.
There were a few bits from Uncle Bob that started to point me in the direction:
This bit from Giving Up On TDD…
UB: Forget about tests. If you have one part of your system that breaks whenever you change another part of your system, what can you conclude about the design of that system?
Well, you’d probably conclude that the part that breaks is tightly coupled to the part that changes.
UB: Right. And that’s what you experiencing with your tests. The tests are tightly coupled to the production code.
Well, sure, that’s normal isn’t it? I mean, tests really do have to be coupled to the production code, don’t they?
UB: Not so coupled that they break all the time. If many of your tests break every time you change the production code then you have over-coupled the tests to the code. You have a test design problem.
A test design problem?
UB: Yes. You haven’t properly designed the interface between your production code and your tests.
Wait. What? There’s an interface between the code and the tests?
UB: Of course there is. And that interface has to be designed.
But isn’t the interface between the tests and the code just the low level functions inside the code that the tests are calling?
UB:They are if you want to have fragile tests. But if you want to properly decouple your tests from your production code, you design an API for those tests. By the way, that API will also be the API that other layers of the system use to communicate.
He echoes this in the latest blog post:
It, frankly, took me many years to realize this. If you look at the structure of FitNesse, which we began writing in 2001, you will see a strong one-to-one correspondence between the test classes and the production code classes. Indeed, I used to tout this as an advantage because I could find every unit test by simply putting the word “Test” after the class that was being tested.
And, of course, we experienced some of the problems that you would expect with such a sinister design. We had fragile tests. We had structures made rigid by the tests. We felt the pain of TDD. And, after several years, we started to understand that the cause of that pain was that we were not designing our tests to be decoupled.
If you look at part of FitNesse written after 2008 or so, you’ll see that there is a significant drop in the one-to-one correspondence. The tests and code look more like the green design on the right.
This is when I realized I have a test design problem.
The most telling was when he flat out says in the interview with James Coplien that he stopped using the word “unit” in front of “test”. He also mentions at one point if you have 25~ or so mocks, that’s a lot, you should have probably less than 10 for an average module and only mock across architecture boundaries instead of every. single. dependency. a class has.
This is where his examples in his C# book came in big time to help me starting using this in practice. As a pre-req, you really need to understand his Clean Architecture implementation. A good video on it is here.
After watching that YouTube video about 25 times, reading the examples in the book a few times, and then applying it to two projects, I finally have a solid grasp on how to really design software and make sure the tests and production code are decoupled. I now only have two mocks in the module I finished refactoring and I have complete code coverage without a unit test for every. low. level. function.
I even have a very fast test suite that can run in about .500ms locally as I’m developing. Meanwhile, just by swapping out a few mocks, I have a full integration suite that uses the live database and APIs that can be run by the CI server and takes a couple of minutes to run. It’s damn near imperative to have the test suite run super fast locally or else no one will run them. And with his architecture and approach, you basically get this for free:
- A way to run a local test suite very fast
- A way to test business rules without external APIs and databases.
- A way to test the UI without hitting live APIs and databases
- A full integration test suite with the live APIs and databases.
It really is a different way of thinking about building software and makes your head spin at times. And as he mentions at the end of the post - TDD or no TDD - you can still end up with sloppy code and bad architecture, it’s up to the programmers to design and good one will make sure the tests are part of that design.
With that said, it was also very hard to introduce this to other programmers until I had solid code laid down and concrete examples to show.