RIP TDD
facebook.com
facebook.com
Testing does tend to have a negative impact on API design. If we were to make a list of positives and negatives, this one would go in the "negative" column. Listing out TDD's positives does not invalidate that negative.
It's really not. Objectively, this wouldn't even be within artillery range of the front page if it hadn't been written by a Big Name(tm).
I even have what borders on a fundamental disagreement with his last point :
> Anxiety. Perhaps what I'll miss most is the way TDD gives me an instantaneous "Is Everything Okay?" button.
This point of Kent's feels particularly weird given that DHH had specifically criticized the TDD artifact that is unit test worship & mock abuse. That workflow practically embodies the notion of a "false sense of security". It might do wonders for personal anxiety, but I have doubts about its ability to rigorously test a system :/
This is doubly true of the DHH article Kent is rebutting.
I think this sentiment is the single root cause of the current back in forth. In my experience this is completely untrue. For DHH and others there appears to be some sort of culture of dogmatic polluting of APIs for testing which I'm not familiar with.
Everything else seems to be talking around the real issue and finger pointing.
So, in order to use (constructor) DI you would have a constructor that looks like this:
public InvoiceController(InvoiceDao invoiceDao) {
this.invoiceDao = invoiceDao;
}
The key point to remember here is that this has been done so that in our unit tests we can inject a fake InvoiceDao. Now, let's say that our InvoiceDao class has one and only one constructor: public InvoiceDao() {
// sets up connection parameters, etc.
}
Then the constructor for InvoiceController could be simplified to: public InvoiceController() {
this.invoiceDao = new InvoiceDao();
}
This is quite a bit cleaner from an API perspective, and that is the entire point. This is only a simple example. For more complex classes, with multiple dependencies, it really becomes cumbersome. What if InvoiceController also needed access ReporterDao? Well, then you need to add that as a parameter to the constructor as well. Your API is made more complicated, all in an effort to make testing possible.This does not, of course, invalidate the benefits of unit testing, which are many. But it does expose a negative that is not frequently acknowledged, and that's what DHH is talking about, and what Kent has failed to address.
I disagree with this central premise. If you have introduced DI simply for testing reasons than you have missed the point. The reason for DI is that a very common point of change with software is at the interaction between clients and services. Tight coupling at this point of change is usually a design smell. That DI enables automated testing is a by product of it being loosely coupled not the point of the loose coupling. That TDD drives you to this sort of solution, all other things being equal, is one of the advantages of TDD.
If DI has become cumbersome and is making your code hard to reason about/read/maintain etc then blame DI, not the testing. You also might want to consider the higher level architecture of your solution if you are finding yourself with lots of complicated dependency chains.
The problem with TDD is that if it is NOT a common point of change, TDD adherents often say that you should introduce DI anyway to make testing easier. "One day you might want to change out the whole database, so you should abstract out the DB into a blah blah blah pattern, with a layer of the rah rah rah pattern between to ..."
To the extent that's a problem, its not with TDD, nor with DI, but with the particular people giving the advice. Why do people think that "Some of the people who follow X give bad advice Y" is a criticism of X when Y is not something that that X requires?
Test-Driven Development by Example, Kent Beck. This came out in 2002 and I haven't read it since then but at the time it really influenced a lot of my TDD thinking. I expect if I read it now I'd have some complaints based on my decade of experience with the process.
Growing Object-Oriented Software, Guided by Tests, Freeman & Pryce. Much more recent and includes more modern thinking about TDD with acceptance/integration tests.
Working Effectively with Legacy Code, Michael Feathers. A great book for dealing with TDD when you aren't doing green field development. He is a bigger fan of Mock Objects than I am, but he illustrates some examples when mocking is the most appropriate response to the current requirements.
All that said, TDD is like any other software methodology. It is a set of patterns and principles that each practitioner interprets for themselves. At it's core though it's pretty simple, automatic verification of specifications are as important as implementation of the specification for any sufficiently long lived, complex software system. Writing those verifications first provides, design, process, and management advantages over the historical process of writing them last and has a high correlation with well factored code. That's it, no doctrines about unit vs integration tests, mocking out DB access or 100% code coverage.
Where is the data supporting this claim? I don't believe it is true.
"That's it, no doctrines about unit vs integration tests, mocking out DB access or 100% code coverage"
Why do you think so many of us feel that it is an ideology? Are we seeing ghosts and imagining the whole thing?
This is one central premise of TDD and is not proven or disproven yet. What we can say is that previous software methodologies were lacking, and cannot prove or disprove their superiority over TDD with regards to this statement. If you distill all debates about TDD down to their essence, it revolves around this premise.
I am fine with someone being skeptical of this claim, but I would prefer if they offered A) a measure we can use to test this hypothesis and B) a contrasting methodology that performs better with regards to the measurements provided in A. The single biggest outstanding problem in software engineering is finding a metric by which we can judge software quality objectively. Because it is such an elusive goal other less precise proxies for software quality have been proposed to stand in for the more complex metric. TDD hangs it's shingle on "testability". Even though this has obvious defects, I've seen no evidence for any other objective measure as a more precise indicator of software quality and quite a few advantages to "testability". Namely, it is measurable.
"Why do you think so many of us feel that it is an ideology? Are we seeing ghosts and imagining the whole thing?"
No, I'm sure that you have encountered well meaning but flawed practitioners of TDD. Your skepticism of their process doesn't bother me. What bothers me is your (and DHH's) painting of all TDD folks as cultists. I don't believe this stuff because Uncle Bob told me to believe it. I believe it because my experience shows that rigorous use of TDD practices trend toward better software than a lack of rigor outside of an objective measure for software quality.
If you have an alternate rigorous approach that you believe trends (or better yet is provably) better in software quality, by all means outline it. I know that TDD is flawed and am happy to find its successor.
Let me ask you this, prior to the rise of TDD, how prevalent do you think automated testing was? If it was very prevalent why is it only after the rise of TDD that automated testing became a central part of any build workflow and the entire concept of Continuous Integration/Deployment developed?
In my experience, automated testing prior to the rise of TDD was not wide spread. But maybe I was seeing ghosts and imagining that.
Bob is not an obscure figure by any stretch of the imagination. You have admitted to having no real data to back up the claim that writing tests first leads to better code, yet we have bob here telling us we are absolutely wrong if we don't follow his religion.
Probably because you've run into cargo cult practitioners that treat it that way -- the same as every methodology that's become known outside of a narrow initiating group has -- and human perceptual biases tend to overemphasize and overgeneralize the most extreme examples.
jump to the 58 minute mark. http://m.youtube.com/watch?v=WpkDN78P884
The premise of TDD (and one I've found true) is that there is a natural correlation between easily tested code and well factored code, not that you should be compromising your design for testing. Thus the original comment about bad api design as a result of TDD rings false with me.
You have apparently encountered cases where people have introduced unnecessary DI in the name of ease of testing when in reality they should have been fixing their central design problems. Software with long dependency chains and complex IoC containers are not easy to test, though looking through the comments it seems that many people have fooled themselves into thinking they are.
Really? Because I've always seen DI advocated for loose coupling, with testability just coming along for the ride the way it does in general with loose coupling, I've rarely seen it advocated for testability independently of the broader architectural motivation for loose coupling.
These are "enterprise" Java apps with Spring, etc.
A good DI container can make it much easier to follow a pretty good amount of general best practices. Single Responsibility, DRY, and Open-Closed Principle to name a few.
I've had better luck and cleaner code without it than with it.
DI is solving a problem common to non-trivial OO apps - where do you create the objects? Every sane approach to this tends to lead to a form of DI.
Spring is a bad example of DI because it encourages an implicit, DI-everything style. Take a look at Guice or other frameworks where bindings are explicit.
Because a testable unit of code must have its inputs and collaborators controlled, all the inputs and collaborators need to be configurable or replaceable, even when such configuration is unnecessary to the business value aimed at.
In statically typed languages like Java, this tends to exhibit as a proliferation of interfaces that only have a single implementation, namespaces with too many identifiers publicly exposed, and lots of open extension points - which hurts versioning. It tends towards requiring complex IoC controllers with associated config. Code ends up filled with boilerplate glue and over-abstraction (factory-factories etc.), and much harder to read because IDEs can no longer follow the link between a method call and its implementation - because the method call is all too often dynamically resolved via an interface implementation.
You mean "White Box Testability". Don't forget Black Box testing is agnostic to the implementation.
As long as people throw out a simple "the real issue" without explaining what they mean by it, it's inevitable. Pretty soon, every player is talking past one another, someone writes a blog post purporting to lay out the landscape of all sides, someone counters rap-battle style, someone else posts to HN "Ask HN: your favorite reasons to TDD," which provides the foundation for an ebook, after which three people post "Show HN: site to plan an optimal TDD strategy." After that, we wait for someone to posit a successor and the cycle starts over again.
If I have a controller that calls a service, and a service that calls a dao, then the service needs the dao injected:
public Service(Dao dao) {
this.dao = dao
}
Which means (in the absence of autowiring) that the controller needs to be "aware" of the dao: ...
service = new Service(new Dao());
result = service.serviceMethod(param1, param2);
...
And that is objectionable because of putting too much of a burden on people writing controller logic, because of separation of concerns, etc. I am finding it difficult to argue against that point.I get that this goes away if using a proper inversion of control container so one can autowire, but that is not always practical.
No, the controller needs to be aware of a method of creating new services. Depending on the specific language and other constraints, that might be a factory function passed into the controller's constructor, a factory object passed into the controller's constructor, a refernce to a service locator object, or some other mechanism. Specific direct knowledge of the DAO class is generally neither necessary nor desirable.
It might be clever but it's unintuitive and as a consequence developers (even otherwise experienced ones) who have not yet mastered AngularJS will write the kind of mess that is painful to clean up later. This is more or less true for any technology, but as a general rule the more complicated or different the concepts are, the less likely it is that people will get things right the first few times. And this gets worse as the application gets larger and / or more people get added to the team. A simpler approach with less cleverness would help.
Not all test has a negative impact. IMO, certain forms of testing have a positive impact because it provides feedback and allows fearless refactoring.
White Box testing with many mocks and other affordances has a negative impact.
Black box testing does not require many affordances.
I'm not sure I've ever seen a debate about TDD without at least one TDD advocate conflating testing in general with TDD…
Also, DHH argues against plenty of other best practices / design patterns and is considered incorrect in many of these cases (his "only use the Rails classes" objections to having a service layer most famously). It just so happens that when he bashes something that a lot of developers don't like to do already (write unit tests), the pitchforks rally behind him.
It's the same sort of thing when people post articles on HN bashing pair programming, or open workspaces. We get it. Some of you really don't like change, or anything "agile". Now please stop beating the dead horse already.
That's not a straw man: That's real life. I've worked with people like this. They're fairly common in TDD shops. You even see them on Hacker News making spurious claims like how TDD would have prevented the <security hole> in <framework> (nevermind the fact that that <framework>'s dev teams is heavily invested in TDD)
I've worked jobs where it was my responsibility to salvage undocumented, untested legacy code and extend it with new features while keeping it running.
The truth is rarely in the extremes being characterized in these blog posts and discussion threads.
The only kind of test code I've seen that required far too many mocks and dependency injections tend to result from poor design choices and writing the tests after the functionality.
If TDD as a practice doesn't work for you, that's fine -- but I think it's disingenuous to offer no alternatives. Integration testing isn't an alternative; I use them in concert with a well tested code base. And I use whole-system tests too.
DHH works in a small corner of the field. TDD may not work well for Rails applications is all I've been able to glean from his posts. I think that's a battle for the Ruby/Rails community to have. It doesn't discredit TDD one bit.
And even this is not the case - the Ruby/Rails community is heavily involved with TDD/testing frameworks/etc, because it is so easy to do and because unit tests can help bring some of the assurances you miss from static typing back into your systems.
The issue DHH has with TDD is, imo, all wrapped up in the issues he has with design patterns, service layers, etc. Doing TDD on a business domain object that subclasses ActiveRecord::Base forces you to mock/stub all sorts of DB calls (which is irritating), which in turn highlights the fact that, contrary to what DHH says, it's not always a very good idea to stick all of your business logic in the model layer.
DHH isn't having any of it, but by no means is he representative of the Ruby/Rails community here. (see: https://www.destroyallsoftware.com/screencasts/catalog/what-... for just one of many examples)
Thanks for pointing that out though; I didn't intend to assert
dhh's opinion <=> rails community's opinionNote that this is not necessary for small, simple Rails applications: by all means, if you're writing a blog app, stick everything in AR::Base and don't over-design. But once you start working in large-scale, non-trivial Rails apps, it becomes very painful to have to have all of your tests running against your database for example.
This is probably something of an idiosyncrasy of Rails, and may have little relevance to other frameworks / languages.
But yeah, they exist
I'm not sure if you're exaggerating a little, but I think that having colleagues whose wrath makes you hesitant to express yourself is an entirely different issue from that of test-driven development or not. Development is a fairly easy field in which to demonstrate that you're at least as correct in your methodologies as another developer: just produce quality code. This feels (as these arguments very often feel to me) like a personality issue more than a methodological one.
There's more than one way to skin a cat, and it isn't always fear of change that keeps people from changing.
When I see "agile" and "tenet" in the same sentence, I think "government shop". An explicitly flexible premise (Agile) does not have "one true way" to do anything.
Dear god, please take the straw man out back and shoot him already.
There really is no way to have an intellectually honest discussion about these subjects, is there?
I span that weird space between product and engineering though. So most of the time I'm hacking in something like "prototype" mode, so TDD just makes no sense. I approach it more like sculpting. I'm slowing hacking away at this block of marble to produce an elegant "beautiful" product. Once it start to take shape I can create tests and specs to define the learnings and creations that I've made.
Tests first? It just doesn't work for me.
If anything, if the adherents of TDD were to properly abandon it, then that would be TDD dying because they would have come up with something better or moved on.
Hacker News is not the audience for this post.
Kent Beck is doing damage control.
DHH lives in a developer community that has adopted unit tests to the point of feeling ashamed about untested code---testing is more than just a practice, it's a culture.
Kent Beck lives in a world where the mere existence of unit tests is a champagne-worthy surprise.
There are people out there who are not yet convinced of the benefits of tests, let alone test-first or test-driven design. Kent Beck is a missionary, bringing them the gospel of automated testing. Can you imagine the impact that a piece like DHH's would have on his efforts ?
A developer with an incomplete understanding of software tests, reading a post by a big-shot recognizable name that one would expect (based on the Rails community's love for testing) to be a major proponent of tests, would take it as "Tests are actually a bad idea !"
This is not what DHH said. There is probably no one among us here who would understand it this way, and most experienced folks would just shake their heads at the ongoing back-and-forth and resume their position of "TDD is a tool in my toolbox, and I use it whenever it helps me."
Kent Beck wrote a piece for the lost soul who doesn't know the difference between TDD and automated testing, and who might become confused after reading DHH's opinion. This is no time for subtlety, for paying notice to the differences between testing methodologies, or for polite agreement with at least some parts of the "opposing" piece.
Let David work to bring the Rails community back from the "test all the things!" extremities it might have reached, let Kent work to bring the unenlightened masses out of the "tests are useless!" darkness wherein they have dwelt for so long, and let us accept that if we truly have the capacity to criticize what those two are saying, then they probably weren't talking to us in the first place.
It's more "contemplative" - thinking about invariants of your software rather than designing for mocks and various injected components.
(although property-based testing is a bit less effective in languages that don't make type information available, to generate the input values)
/downloads
Shouldn't the URL here be simply: https://www.facebook.com/notes/kent-beck/rip-tdd/ rather than https://www.facebook.com/notes/kent-beck/rip-tdd/75084019494...?
Does every element that exists on facebook.com get assigned a unique string like the one above?
The advantage of only looking at the number is that if Kent Beck's name changes or the title of the note changes, they can just start linking to a new URL with a changed human-part (but the same numeric identifier) and the old URL will still work. The "correct" way to do it would be to track and validate every version of the human-part for every URL, redirect old versions to the newest version, and 404 made-up ones. But in a way that would be slightly inconveniencing human users following outdated links by making their browsers follow a redirect when really only search engine spiders care whether there's a single canonical URL per page, so if Facebook has given it any thought, they probably decided just to not bother.
And at their scale, keeping track of one or more string URL versions for each page (that is, each piece of URL-addressable content that makes sense to have some kind of title slug included) would be non-trivial. Billions and billions of URLs to track, dubious benefit.
I don't know how many times production was saved because there was an automated test written via TDD that prevented bad code from going out. Too many times to count.
What the author is suggesting is to throw this out and go back to the days of broken production apps are okay. This has never bothered Facebook, and it will definitely bother engineers who want to take pride in their work.
"Over-engineering. I have a tendency to "throw in" functionality I "know" I'm "going to need". Making one red test green (along with the list of future tests) helps me implement just enough. I need to find a new way to stay focused."
So creating a test for a function that sum of numbers that gets 1 and 1 and returning a hardcoded 2 is "just enough" and " a great way to stay focused ", funny
I kind of agree with the other points, not so much about the "API design" stuff, while it's true it facilitates, the end word is with the main API user.
Well, I'm glad it's dead, now I don't have to spend time mocking all other parts of my program.
I think now is an important time as ever for TDD. (Test your expectations! You might be surprised or even wrong. Do not underestimate your hubris when it comes to introducing bugs.)
1) People make money on these wars. Do not waste your time on them.
2) Out of all the Agile stuff I've ever come across, TDD is the thing that drives most people nuts. I think because it's something that has to do with your minute-to-minute work. I'm a fan -- when it's appropriate (not in startups). But dang, people go crazy about it.
3) TDD is more about the way we approach doing a thorough job than it is about programming. At least in my mind. It's double-entry bookkeeping for coding. Not only did you do it, but it's cross-checked. Therefore, just like good accounting practices, it makes sense to a lot of people. It's also annoying as hell to a lot of people. This set probably overlaps.
IMHO fat models are the new PHP, and giant classes suck. Its time for new conventions to emerge.
Prove DHH wrong with working code, not blog posts or books or conference talks.
I completely agree; I will only say that it actually is quite easy to add service layers and fast tests to Rails applications - you just don't stick everything inside ActiveRecord like DHH would like you to :)
The only "framework" part that you need outside of some reasonable OOP design principles is some amount of type checking to enforce some notion of boundaries. Ruby is super weak in this area, so I added some bits to do that.
For the record, until you experience the trouble that the MVC Big Ball of Fat Models or Controllers gets you to, you probably won't have much appreciation for the service/command style or the hexagonal ports/adapters style. At least, that's been my experience with other developers.
To the whole article. I read the dhh article, Kent did not. The people practicing umm "agility" did a better job handling some of the root issues DHH brought up.
The critics, including Kent Beck here, have by and large reducto absurdum'd it to "DHH HATES TESTING AND EATS BABIES", which isn't his point at all. Sure, DHH could have been more narrowly focused and less confrontational in his language, but that doesn't mean he's wrong.
Look at it this way... are controllers hard to test in Rails, or are controllers hard to test in general? This is an argument for lightweight controllers that are little more than routing glue (don't put business logic in controllers), and then giving them a pass on unit testing in order to keep them clean.
This gets to something I've been chewing on and should write about, which I think of as Tautology Testing. It's endemic in excessive unit testing. You create a mock so you can create a test so a test exists. The test doesn't really test anything except its own existence. How is that useful? You're just adding complexity, not functionality.
[1]: http://david.heinemeierhansson.com/2014/tdd-is-dead-long-liv...