The Pragmatics of TDD
blog.8thlight.com
blog.8thlight.com
If you write the test first, that can't happen, because you haven't written the actual code yet.
For instance, as danso says at [http://news.ycombinator.com/item?id=5332448], it forces orthogonality, because you have to think how a function can be tested independently of the external objects that may use it; if you have to write your code with automated tests in mind, it's almost inevitably going to be more modular, because poor modularity turns writing those tests into a massive pain.
Also, the tests can act as something like a formal specification; in the process of writing tests, unspecified edge cases may become apparent that weren't noticeable in the natural-language spec.
So, the code provides a list of numbers 1, 2, 5. Non-TDD would code a test that verifies the list is indeed 1, 2, 5.
However, the business wanted the list in reverse order: 5, 2, 1. TDD would force the developer to state this up-front. A non-TDD developer is more reluctant to change the code, since "it passes the test!"
I've done this, although I like to think I'm quite happy to rewrite something if it's not good enough (or just wrong).
No one is advocating releasing broken stuff, but whats the point of releasing something perfectly tested that no one wants? I think the answer lies somewhere in the continuum. And in my own startup (wello) we do go back and write tests, because testing everything manually does hurt once the feature is working well and users actually want what you are building. So we do carve out time and write tests and often times then build it right. You could argue that this is inefficient, but I could also argue that for many features we removed because users did not want them, we saved the time needed in upfront TDD.
Regression tests should get refactored at the same time by the same tool that refactors the rest. Code changes should be handled the same way.
I truly believe that TDD works. I truly believe that the jury is already in and that anyone who is serious about becoming a software professional should write tests for every line of code. I really really believe it and want to use it.
That being said, it is so fucking hard to get started with TDD. Oh god, it's so difficult. I've done the katas, done an apprenticeship, read the RSpec book, watched every screencast I could find, everything you can reasonably ask a learning developer to do, and I still find it incredibly impractical to practice TDD when working on most projects, not because I don't want to, but because it's so difficult and time consuming and the resources just aren't there to help make my process quicker.
Here's an idea for the TDD crowd. Every time you're about to write a blog post about why people should use TDD, instead write a blog post about a situation where you applied TDD, the tests you wrote and the code it lead to. We need more examples of TDD in progress, more code snippets, more screencasts. I'm telling you the problem is that the resources just aren't there to encourage these habits. Instead of continuing to have this debate at a semantic level, if there were more testing resources available I think people would naturally flock to it and TDD would win out. Until then, I think it's twitter fights and bad habits for the foreseeable future.
*This comment applies verbatim to security best practices as well.
http://www.letscodejavascript.com/
And if Java and Swing are more your thing, Let's Play TDD is its less-polished progenitor. http://www.jamesshore.com/Blog/Lets-Play/
You're right, by the way. It's much harder to do TDD for real than it is to do all those toy problems that involve maybe one class, some calculations, and nothing else. That's why I created the screencasts.
I'll see if I can dig that up and translate that in a way that's actually useful to an asynchronous audience.
The topic I really wanted to cover but didn't was TDD in startups. I have a simple belief: the value of your code debt can never exceed the value of your code. That is, if you code has no monetary value, it is impossible for you to have any code debt, no matter who you are, what your code does, or what the code looks like. Think about it. It makes sense.
It's interesting that Bob took a "saw the baby in half" approach here, outlining the various things he'd throw away and the various things he'd keep. While I think there are definitely shades of gray, it would also be useful for him to directly address the question of code that has no value. If I write a function that I save on my hard drive and never use, does it need a test? I belief the ludicrously obvious answer is "no", but I haven't heard him say that yet.
Write a blog post about specific examples => "It might work for this example, but in my experience it didn't work in this other case."
If you write in the abstract, people will dismiss it as fluff. If you write concretely, people will dismiss it because it doesn't cover their exact case.
There is no way to please everyone in the world of opinionated software blogging, so stop trying.
The fact that some software development organizations out there aren't using any test automation* even though they would probably benefit from it might make me a little sad, but it's not a problem that I'm trying to solve.
*or source control, or deployment automation, or incrementalism, whatever.
There are plenty of things that most people take for granted now that people previously would not have believed.
With enough supporting data it would be hard to argue against TDD, for example an independent study of a sizeable number of software shops which showed that developers using TDD shipped code 50% faster or with 60% fewer defects or whatever. That would put the onus on the anti-TDD person.
I think Zed Shaw wrote something about this, but can't find the link.
The closest I can find is this, http://research.microsoft.com/en-us/groups/ese/nagappan_tdd.....
Once you have strong backing data , it's not snake oil anymore and it becomes increasingly difficult to make that claim.
Anecdotes? Obviously not OK.
Small-scale controlled experiments can be dismissed as nothing more than pointless toy exercises that have no bearing on real-world production code.
Large-scale controlled experiments are impractical. Blinding and placebos aren't possible. Isolating confounding elements such as individual performance would be difficult.
Burden of proof is burdensome, I guess.
I'm new to it but I find it an incredibly useful strategy. It forces orthogonality on me because I have to think how a function can be tested independently of the external objects that may use it...which causes me to challenge my initial assumptions and instincts about the overall application design.
In some sense, I guess, it is always frustrating to spend more time in the design stage than working with an actual prototype...but I find the medium-to-long term benefits to far outweigh the initial investment in time. And once the tests have been written, the actual functional code is almost trivial to write.
Even without the benefits TDD has in easing the maintenance/upgrade phase of a product, I find its effects on the design/prototype stage to be worth the effort alone.
In general, just call the real thing and don't worry about it. Otherwise you'll expend loads of time and energy setting up fakes to test relatively simple code.
Because it's expensive. It takes time to write and maintain those tests. It takes time to run those tests. If you want to verify that API method works as assumed (and I assume you are talking about an API on an external system, but ultimately it doesn't really matter) then write a test specifically testing that API method.
Testing that method B also works while testing method A is, in my experience, the number one culprit of people writing bad tests. These tests are harder to set up, take longer to run, and are more prone to promoting brittle test code.
Test everything that's important to test, and nothing more. This is true whether you are talking about your entire system, or a single method.
It's very tempting to start doing this when you get into heavy unit testing, but I find in practice this adds more cost than value.
As an example, lets take some python code I just made up which formats some input into a message, throws the message onto the queue, and returns the message ID.
class DelayedJob(object):
def __init__(self, queue):
self.queue = queue
def add_job(self, task, params, priority=1):
m = JSONMessage({
task: task,
params: params,
})
self.queue.add(m, priority)
return m.getID()
It's pretty clear that the queue service needs to be mocked out, as you don't want to be talking to a real queue for a unit test (you'll probably want some sort of acceptance/integration test to cover this somewhere).However, I have in the past been tempted to make the JSONMessage class injectable, and then inject a mock with a stubbed implementation of getID - I've often seen other people do this as well. The code required to set-up such a fake would be fairly verbose and add little in the way of extra clarity to the test. Since the class is fast and simple (in this scenario it's just a container) then I'd just leave the concrete class in use.
I've seen startup devs who couldn't cope with changing requirements fast enough. There was just too many moving parts at the same time.
Hell, I've been one of those devs! Great code is the exception, not the norm.
When I learned TDD, it greatly improved the quality of my code. If TDD is a bottleneck for you, maybe you need to learn to touch type.
My 2 cents.
TDD is one of the few actual disciplines I know of around coding that can't be just glossed over or faked. One either does or does not. This is doubly true in a pair programming environment. Not using one of the established patterns? One can argue around that. Not doing TDD? Justification usually has to tie with UI in some way.
TDD is trivially easy to fake. Perhaps worse, it's trivially easy to think you're doing it, when you're actually doing nothing constructive.
In any kind of setting with shared code, it shouldn't be a matter of self-discipline, it should be a matter of standards and best practice. You shouldn't be integrating any code without test coverage. This is a standard part of how many projects handle pull requests.
If you're only accountable to yourself and writing the tests first is the technique that works for you, I can't criticize but you really don't have much to say to programmers in general, either.
While not, NOT a TDD approach, it did lead to a much higher code coverage and added a safety net when I had to change it: I knew I could change the code, and if the result was materially affected, I'd know.
It did slow me down in the initial writing, but in the revisions, it sped me up, IMO.
Seriously, the current status quo most of us live in right now is zero to very few automated tests. That's what I'm arguing against.
I do not claim trascendence. It's just what worked for me. I've never done TDD at any time in my life. Note, after reading the arguments for it here, I think I will give it a spin.
One thing I noticed though, is that if you don't write tests before hand, you don't have time to write them later. Your code tends to be harder to test too, so you never really write them (costs too much).
To me, it's not just about writing tests though. It's about writing an executable todo-list of your features.
Before TDD/BDD, I used to take the time to figure out everything I had to do with their relationships together. Most of the time, it would change during the implementation as I forgot about something important while "designing". Also, I used to implement 2/3 things together because they were related.
Now I only create a new test and make it pass, then I refactor if I need to and proceed to the next one. If a new idea comes up I write it down on a piece of paper for a future test.
This way, my Work In Progress is always 1, before that it was around 3/4 maybe. The mental overhead is much lower because I can rely on all the other tests I made to ensure I did not break my feature.
I'm not advocating pure reliance on TDD, but for 95% of the average developers out there, this is definitely an upgrade.
This particular argument about the supposed efficacy of TDD depends on people accepting the equivalence of the efficiacy of TDD with the efficiacy of surgeons washing hands, just because the blog author says so.
Oh horror, how can you challenge my dogma just because I call it a 'discipline' and verbally equate it to surgeons washing hands.
Saying something is true doesn't prove it is true whether you call something a 'discipline' or not. Cults are full of such 'disciplines'. Cult members will vouch for them. A better word is 'ritual'. At best such rituals are cargo cult practices. [1]
Surgeons washing hands is a practice with empirical, unchallengeable scientific evidence supporting it,while TDD is a dogmatic practice evangelized by software process zealots, with next to no scientific evidence backing it up.
Also TDD != testing and TDD != automated testing (though the evangelizers tend to blur the differences. It is easier to argue that you should write tests, than that you should write tests before you write the code, which is a somewhat more shaky assertion. If you can paint your opponents as opposing tests (vs TDD) you have set up an easily knocked down strawman).
Programmers have written tests, including automated regression tests for decades without the blind adherence to the 'write a test first, write the code, refactor, repeat' cycle that TDD consists of.
Insisting on this as some kind of moral imperative [2] is snake oil, and it is natural that experienced devs push back against religious preaching.
The best 'poke holes in the zealotry gently but firmly' writing wrt TDD is at http://www.dalkescientific.com/writings/diary/archive/2009/1... . Bob Martin's TDD 'kata' is dealt with there in some detail. The comment thread at http://dalkescientific.blogspot.in/2009/12/problems-with-tdd... is hilarious too, with some familiar names popping up.
[1] http://en.wikipedia.org/wiki/Cargo_cult
[2] The author says as much here http://news.ycombinator.com/item?id=5331108
" [TDD] allowed us to go fast, and _keep_ going fast because the code stayed clean. I have come to view it as a moral imperative. No project team should ever lose control of their code; and any slowdown represents that loss of control."
Part of the reason experienced devs push back against TDD has to do with patterns and habits. It's safe to say that TDD feels awkward to anyone with other established work patterns.
TDD is of course simply a methodology. It works well for some teams, and perhaps not for others. What it does do is impose a culture of testing and rigorousness, which is rarely a negative.
Sure, you can decouple testing from TDD, but IME teams that apply TDD have more extensive coverage than those who don't. Causation correlation caveats apply.
I personally dislike TDD because it disrupts the thought pattern I have become accustomed to using when developing software. I consider myself relatively junior, so I can only imagine how much of a workflow departure it must be for more senior engineers. However, just because it doesn't work optimally for me (or, optimally at first) doesn't invalidate the approach nor make anyone who uses it some brainwashed cult member.
A couple months ago, in fact, I tripped on a blog post that said (paraphrased): If you do not do TDD, then you should re-evaluate your career as a developer. This is bullshit. It angers me because I keep meeting very smart junior developers carrying around a load of guilt over TDD.
I'm not sure what it is about TDD that make people forget that these things are all tools. You put it in your bag of tricks and pull it out when it makes sense. You don't beat each other to death over what tools they use.
Sorry, no, TDD isn't like vim or emacs. Your choice of vim or emacs doesn't change the code you produce. TDD does. Whether you agree with TDD or not, it's completely reasonable for a job to require it, just as it's reasonable for a job to require that you know and use MVC (in their app), or Rails, or any other technical solution to a problem.
If I'm hiring somebody, they have to know TDD and apply it, or they won't work for me any more. Period. Just like they have to know what local variables are and use them. Right or wrong, it's a coding/design standard, and it's perfectly reasonable for teams to choose and enforce their coding standards.
Now, that person might be able to convince me that they have something that solves my problem (a need for easily-changed code that does what the programmer intended) better than TDD. Say, a fancy type system. If so, awesome! I'll listen. But "I'll use TDD when I feel like it" ain't gonna cut it.
TDD is a workflow implementation and not a coding standard per se. This is to say, code written with or without TDD may be completely interchangeable. This is not true of a coding standard.
Coding standards measurably and objectively affect the end product (i.e. "use local variables", "space/tab indentation", "no method greater than 500 lines", "follow an MVC pattern"). Can the same be said for TDD or [insert workflow implementation here]?
This may be a nitpick, but I feel it's important since it highlights why there is such disagreement on this topic.
Think of it this way. Imagine two different teams. One team has a workflow that involves carefully considering design possibilities and creating design models on paper, and only writing code after those design models have been iterated and refined for a good month.
Another team dismisses that approach as waste, and instead prides themselves on their ability to ship. They start coding on the very first day. Although they care about design, their emphasis is on shipping code, and they would never waste time on modeling.
These are workflow differences. Will the teams produce different code? Of course! They approach the work differently! That's what workflow differences mean.
TDD is a different workflow that produces different code. That difference matters.
(See also this reply elsewhere in this thread: http://news.ycombinator.com/item?id=5334366 )
Edit: I don't mean to say that TDD will lead to good code. Far from it. TDD done badly leads to some gawdawful messes. But TDD done well does lead to code that is qualitatively different than other workflows done well and that difference--the result of using TDD--is useful to me.
Who cares if the code is clean and the project is successful? TDD may be a useful way to get to clean code for /some/, perhaps /most/. But this is not true of /all/. I'm not arguing against its usefulness for someone like you. That's silly, you obviously value it. I'm arguing against the "moral imperative," which is utter nonsense.
TDD is neither necessary nor sufficient for success, and we should stop pretending that it is. I create a class, it has an interface with a purpose, and I test that interface. I don't write tests first, and I don't have trouble writing tests after I write the code. I organize my thoughts on pen and paper, and use very light documentation to test the size and purpose of my methods. The projects I work on are successful, everyone's happy. This is true of several other programmers I work with on a daily basis, but not all. Several also practice TDD, and it works for them.
I said "It's reasonable for a job to require TDD... because the TDD workflow results in a qualitatively different code base." I chose an artificial example of how workflows change code to make the point obvious, but the real-world effect of TDD is night-and-day obvious in my experience.
I value that difference to the point where TDD skill is a core part of my hiring decisions. I'm okay with you making different choices.
Easily changed code that does what the programmer intended is better described as "good design". TDD is a useful design aid, but there's a lot more to good design than just TDD. If you are hitting that requirement, it is because you have good programmers, not because TDD is magic. Fancy type systems don't do much to ensure good design, either. Just like a really nice saw doesn't guarantee a beautiful cabinet.
See: http://peripateticaxiom.blogspot.com/2006/05/complexity-and-...
However, the skeptic in me can't help but point out that this is not nearly rigorous enough to say his method can pretty reliably determine TDD vs non TDD code in the general case.
It wouldn't surprise me if those were pair-only or pair-frequently shops, at which point it makes sense to have a common workflow & toolset across the team. Given the amount of cultural overlap TDD & pairing have (particularly in Ruby-land), this seems at least plausible.
> A couple months ago, in fact, I tripped on a blog post that said (paraphrased): If you do not do TDD, then you should re-evaluate your career as a developer. This is bullshit.
Agreed. However, there's a lot of bullshit on the internet on just about every conceivable subject. Not having read the blog in question, I wouldn't put too much stock in this post representing the opinion of the majority.
> I'm not sure what it is about TDD that make people forget that these things are all tools. You put it in your bag of tricks and pull it out when it makes sense. You don't beat each other to death over what tools they use.
Disputes over tools has nothing to do with TDD and is certainly not unique to it. I would need a spreadsheet or small database to keep track of the varied topics I've seen developers evangelize and/or get into emotional near-venomous arguments over. It didn't start with TDD, and will certainly not end with it.
The appropriate response across the board is typically empiricism, and grounding all discussions in the axiom that Data Wins Arguments.
When looked at in this light his advice is spot on for me. The kind of projects I work on would quickly get bogged down and have high failure chances without automated tests. When my current employer was in the startup phase they ignored testing, were making tones of money, hired tones of devs and wrote tons of code. At least they wrote tons of code for the first couple years then they hit a wall where it became incredibly difficult to add new functionality. Now that they have been writing automated tests for 5 years new functionality is comparatively easy to add to anything but that original project (which is still running making money but very hard to change). This is server side financial processing, user management, business logic code.
Of course one of the disciplines I'm talking about is TDD.
We're talking about Uncle Bob here. If Uncle Bob doesn't realize there's a difference between TDD and automated tests, then there's been a whole lot of arguing about nothing for the last few years.
In other words, no, you can't interpret Uncle Bob that way, he really is talking about TDD.
It's a metaphor, and as such, it's imperfect. Let's give people the benefit of the doubt in their ability to understand that.
The funny thing about this is when I read this bullet point I thought, "Bob Martin would disagree with this". His rules for TDD ( http://butunclebob.com/ArticleS.UncleBob.TheThreeRulesOfTdd ) would force that method to be tested whether he wants to or not. Then I scroll down to see who wrote the article... Bob Martin
EDIT: But maybe he meant to emphasize "indirectly". Maybe it was under test at first but then got extracted into a simple method under refactoring.
The rule I follow is: Every line of code you write is to make a failing test pass. If that means writing three new functions, so be it.
> In general I don't write tests for any code that I have to "fiddle" into place by trial and error.
It is congruent with a some general rules I follow to decide if I should write a test for something:
1) Do I care if this code doesn't work on the non happy-path? Maybe I'm writing some isolated prototype code that will probably be thrown away, or perhaps I'm planning to rewrite it later. I'm not going to bother writing tests.
2) How important is it that this code is bug free, and how soon should there's a bug. If, say, the code is in a highly used part of the program that is required by the rest of the app to function properly. Or it is part of the main feature set. I definitely will write tests.
3) I'm under time constraints. If I don't have the time to write tests for a particular part of the code, then I don't have time.
4) How solid is the architecture/interfaces? If I expect them to change quite a bit, then I will not write so many tests, or perhaps any. I once worked on a project that was heavy into unit testing/tdd. There was hundreds of tests written very early on in the project, and since the code was changing so much, we spent a lot of time re-writing tests. It eventually got to be a huge time sink.
5) How much money does the project have? Writing lots of tests takes time, and time is money. I've worked on some projects that have budge (or time) constraints, so I don't have the luxury of such dogma.
All that said, I often dislike programming religions or dogma, because they often advocate following a practice somewhat blindly, without understanding when the rules/precepts should be applied.
Learning requires investment. Investment carries risk. Risk aversion/tolerance is a very personal and contextual thing. There's almost no point arguing about when it's good to be risk averse and when it's good to be risk tolerant, because of this heavy coupling to the context. Better to be aware of the phenomenon and work things out case by case.
Some people generally don't like to learn. Nothing you do will force them to like to learn. You can invite them to try to learn; you can try to make it comfortable and safe for them. That might work.
Some people find such value in a learning technique that they continue to use it, even after learning 99% of what they will ever learn from it. Continuing to use the technique provides them comfort. Whatever works. Others eventually break free of the learning technique, knowing that they can fall back on it when they feel pressure.
I care about this: people who want to practise TDD should be free to do it; people who don't want to practise TDD should not be forced to do it. Everything else is noise.
If your software has a dependency on a third party component, then you should include that component in your tests. It's not about testing the component, it's about testing your integration with that component. For example, if you upgrade that component, and the API changes in a way that breaks your integration, you want your test suite to break as well.
Sometimes if a component is too slow or requires network access, you have to mock it out. But as a general rule, it's best to leave your dependencies in place.
I think TDD is taught the wrong way around. First, they should teach refactoring. And only when those skills are thoroughly mastered, should they move on to teaching TDD.
I don't disagree, but I often have at least one test that explicitly exercises all of the public interface (of a class/module/whatever). The point is when I change that interface I want tests to break and not have to rely on my memory on what changed when writing release notes / incrementing version number. I mostly test python, YMMV.
Most notably, the measure of "success" in startup-land is not irrevocably tied to code quality. Plenty of projects have succeeded despite their engineering and not because of it.
That's literally impossible to do in a straight one to one comparison. If nothing else, you have different people working at each one.
A broader study might get enough data to make meaningful comparisons, but I think strict TDD is too rare to get more than a few samples. It's a tough problem. I think you'd have to set up a completely artificial environment if you truly want to measure the relative efficiency of TDD.
This is a really good and interesting question, and it's one I've been struggling with myself.
The problem boils down to this: TDD makes your software more maintainable (if you do it well) and it lowers your cost of development. However, it also takes significant time and effort to figure out how to test-drive a technology for the first time. Everybody can TDD a Stack class; TDD'ing a database, or a web server, or JavaScript [0] is a lot harder.
So the answer seems simple: use TDD for the parts you already know how to TDD.
But it's not so simple! It's much harder to add tests to existing code than it is to TDD it from scratch. Sometimes, it's flat-out impossible. The expense is so high, there's a very good chance that you'll never get around to adding tests to the un-TDD'd code. It will hang around causing bugs, preventing refactoring, and sapping your agility forever, or until you rewrite... and a rewrite of any significance will halt your company in its tracks, so you won't do that.
So the reality is that, anything you don't TDD from the beginning, you'll probably never be able to TDD. Companies that go down this road find themselves doing a major rewrite several years down the road, and that's crippling [1].
There's another wrinkle on top of this: manually testing code and fixing bugs is expensive. Once your codebase gets above a certain size--about six developer-weeks of effort, let's say--the cost to manually test everything exceeds the cost to TDD it. (The six weeks number is a guess. Some people argue it's less than that.)
So the real answer is a bit more nuanced:
1. If your MVP is truly a throw-away product that will take less than six weeks to build and deploy and you'll never build on it after that, use TDD only where it makes you immediately faster.
2. If your MVP is the basis of a long-lived product, use TDD for the parts you know how to TDD and don't do the parts you don't know how to TDD. Be creative about cutting scope. If you must do something you don't know how to TDD, figure it out and TDD it.
3. It's okay to be a bit sloppy about TDD'ing the edges of your app that are easily rewritten or isolated in modules. But be very careful about the core of your system.
That's my opinion based on 13 years of doing this stuff, including building five successively-less-minimal MVPs over the last nine months for my JS screencast. The first three MVPs were zero coding, the fourth was a throw-away site, and the fifth was TDD'd with aggressive scope cutting to minimize the number of technologies that had to be TDD'd.
[0] Shameless plug: I have a screencast on TDD'ing JavaScript. http://www.letscodejavascript.com
[1] Rewrites are crippling: See Joel Spolsky's "Things You Should Never Do, Part I." http://www.joelonsoftware.com/articles/fog0000000069.html (There is no Part II, by the way.)