Response to DHH
April 23, 2014
DHH, the inventor of Ruby on Rails, published a blog post this morning in which he criticizes TDD, unit testing, and (vaguely) several software design principles. This is my response.
The lack of a thorough test suite has become a source of intense discomfort for me. I built several apps independently last year and continue to maintain them in production, and fixing bugs or adding features is difficult and stressful due entirely to the absence of tests. I have to be extremely careful when making changes to be sure that everything continues to work as expected. Even minor changes require a significant amount of time, as well as brainstorming all the possible ways some alteration could break something elsewhere in the app.
This concern and stress is almost completely erased when a thorough test suite is in place. Today, for example, I worked on refactoring my Clojure implementation of Tic-Tac-Toe. A couple of my decisions and goals involved large extractions of functions into new namespaces. At times, my git diff was so elaborate it may have seemed I was drifting out of control. However, because I had a test suite in place, I was not only always aware of how far I had deviated from my last working commit in terms of behavior and functionality, but I also had a roadmap of failing specs directing me back to fully functional code.
Thus I was glad to see DHH at least acknowledge “the tranquility of a well-tested code base, and the bliss of confidence it grants those making changes to software.” However, I’m not sure such feelings can be trusted if they rely on tests written after a significant amount of production code. DHH supports system tests that run through applications at a high level, as opposed to unit tests that focus on specific, small, low-level objects (and form the backbone of TDD). Because they operate at a high level, system tests can only be written once low-level components are completed and wired up together. Unfortunately, by the time the application is in a state at which these tests can be written, the developer has incurred a lot of bias and has lost sight of the code’s subtleties.
Consider Capybara, a popular system test framework that provides a syntax that reads and writes like basic English instructions one might provide to a user:
fill_in 'username', with: 'email@example.com',
click_link 'Sign in'. This syntax and style makes it easy for developers to write tests that simulate users interacting with the app. The problem is Capybara encourages developers to write those tests by… simulating a user interacting with the app. The reason this is a problem is users are extremely unpredictable. The developer and/or designer knows every decision that was made during development, and surely believes each was the most logical and appropriate. He or she is therefore biased towards interacting with the app in the best possible way; the developer’s natural path through the app will be the one of least resistance. Users’ paths, however, will most certainly not be so perfect. Therefore, in order to get truly thorough coverage with system tests, one has to guess all the myriad ways users can screw something up. Did you predict someone might try to make a password that is nothing but spaces? Did you imagine a user might hit the browser’s forward button instead of re-clicking the link on your page? There’s just no way to write enough high-level tests to cover every nuance of the low-level components. You need unit tests for that!
Test-driven development leads to more complete test coverage because the tests are written with the minute details of the lower-level components in mind. It’s much easier to account for all the subtleties of some object when focusing on it in isolation, rather than while passing through it from some other object and continuing on to a third. Similarly, it is easier to more fully test an object when it only has one responsibility as opposed to doing many things. DHH bemoans the “dense jungle of service objects, command patterns, and worse” that often accompany applications built in a test-driven manner. In other words, he is complaining about the complexity of design principles and patterns like SOLID. I find this complaint especially confusing; objects that adhere to the single responsibility principle, for example, are far easier to explain and understand than objects that violate it. The “god class” models that Rails encourages often contain so many methods responsible for so many different things that it is difficult to keep track of everything it does. As a result, it’s easy to forget a few things when testing the model after the fact. On the other hand, a class that adheres to the single responsibility principle and that is built test-first is much more likely to be fully tested and easy to understand many months, commits, or iterations down the line.
DHH is obviously a smart, talented, and successful software developer. What do I care if he doesn’t believe in TDD? Well, not much, to be honest. In my pursuit of software craftsmanship, I have been working very hard to focus on things that truly matter and ignore distractions. There will always be someone younger and smarter than me, for example, and there will always be people who believe some language or framework I use is not as good as some other one. If DHH has success building applications that violate SOLID principles and have zero unit tests, that’s great for DHH. But I am concerned by DHH publicly discrediting what I believe to be an extremely valuable practice without offering concrete alternatives that we can consider and potentially use ourselves. He professes to support testing in general and provides some vague comments about system tests and letting fixtures hit the database, but there isn’t much for the reader to take away with regards to how to actually test software. His comments on architecture and design are even less substantial.
Every day, software becomes more important, and every day, more people begin to learn how to code. I believe the vast majority of these people legitimately want to code well, and while much of software development is an art, there are absolutely certain aspects that are a science–that are objectively better or worse. When DHH says “we have some serious deprogramming ahead of us as a community,” I wonder what exactly he means, and how some of his readers might interpret those words. I’m not sure what “deprogram” means, but why would we want to do it? I believe our priority as software developers should be to develop applications well, not easily. The former is a goal, the latter is a wish. If DHH has an alternative to TDD that he can share, I’d encourage him to do so–like I said, many of us are eager to learn and improve. But I don’t see any value in simply lambasting a technique that many of us find productive for no apparent reason.