Test-Driven Development Is Still Alive and Well
In 2014, David Heinemeier Hansson posted an essay on his personal website titled: “TDD is dead. Long live testing.” The post sent shockwaves through the software development world and led to a lot of soul searching. For years, Hansson, the creator of the Ruby on Rails development framework, had been a strong proponent of test-driven development, calling the practice the right way to write code. But in that post, he wrote he was never able to practice it for long. He cycled between adhering to TDD and abandoning it for coding styles that felt more natural to him — and feeling guilty about doing so.
Developers who never liked TDD sympathized. They thought it wasn’t useful to them and that practicing TDD just felt like a chore. Others who enjoyed TDD suddenly found themselves in the unfamiliar position of playing defense, years after TDD had started to become an accepted part of agile development.
What Is Test-Driven Development?
Later that year, Hansson joined Kent Beck, the creator of the extreme programming methodology, and Martin Fowler, the author of many books on software development, in a six-part video series, “Is TDD Dead?,” to debate their different viewpoints of TDD. The discussion raised a lot of fundamental questions about what it means to practice TDD and the strengths and limitations of its usefulness to the software development process.
But in a lot of ways, the debate over the rightful place of TDD in software development is still ongoing. TDD is complicated, with many different interpretations over what it means to do it properly and how to use it to get the biggest benefits for your code. Some developers swear by TDD, saying it gives them the confidence to take coding risks. Others find it too constricting.
Practitioners of TDD talked to Built In about how they make the practice work for them.
It’s Really Test-Driven Design
Brian Riley, principal engineer at digital development consultancy Carbon Five, has been practicing TDD since 2009. Riley said TDD emerged from Kent Beck’s method of extreme programming, specifically from the practice of test-first programming, which states that tests should be written before the code they will be run against.
Writing tests first may seem counterintuitive to those who aren’t used to the concept. After all, doesn’t that mean the tests are always going to fail the first time? Yes — and that’s exactly what’s supposed to happen, said Riley.
“TDD is a cycle of ‘red, green, refactor’ — that’s the classic definition,” he said. The colors refer to unit test results, which traditionally turn red when tests fail and green when they succeed. For each unit of code they write, developers following the TDD handbook would always write the tests first, run them, then let the tests fail. Developers then write enough code to make the corresponding tests succeed and then step back to evaluate their work, refactor and plan their next steps.
The process may sound slow and repetitive, but it’s actually this constricted aspect of the TDD process that appeals to many people, including Beck.
During the TDD debate series with Hansson and Fowler, Beck talked about how much he valued the reassurance of operating within the TDD framework.
“I think there are aspects of my personality that make this piecemeal style work really well,” he said. “I tend to get overwhelmed by big problems, so for me, TDD solves those problems: Even if I don’t know how to implement something, I can almost always figure out how to write a test for it.”
“This process of test-first programming can actually inform the design of your code.”
For many proponents of TDD, its biggest benefit is not strictly, or even primarily, about having tests. It’s that following the practice of TDD necessarily shapes the way the developer thinks about designing and building their application.
“People realized that this process of test-first programming can actually inform the design of your code,” Riley said. “Some people even call it test-driven design. That’s a mindset shift that happens when you’re really using TDD, and for me, it definitely had that effect.”
Steven Solomon, a lead software engineer and engineering manager at agile software consultancy Stride Consulting, said he prized TDD both for catching bugs and its design capabilities.
“As you’re writing code, TDD prompts you to think about your solution to your problem,” Solomon, who is creating a course on TDD, said. “You have to understand what you’re trying to solve, you can’t just write code.”
He characterized this as pushing developers to think more deeply about all aspects of development — writing code, maintaining code and working with other members of the development team or with application customers — all before writing a line of code.
Can TDD Be Used for Complex Projects?
Hansson, during the TDD debate, acknowledged having enjoyed using TDD on projects before but said its effectiveness was limited to certain types of simple projects. It creates extra work when used on more complex projects, he said. As an agile project progresses, the original designs for the application may change, forcing code and their accompanying tests to be deleted and rewritten to fit the changing architecture — so Hansson said it’s better to stick with TDD on projects that are simple and already well-architected.
Wanting to avoid extra work is certainly a valid concern, but TDD supporters tend to disagree that the method causes this problem, arguing that the practice works well with both simple and complex projects.
Madelyn Freed, a full-stack engineer at Stride, said that coming from a non-traditional programming background, TDD is the development style she’s most familiar with. Writing tests first is helpful when Freed needs to try out ideas for how to structure code that she doesn’t have a clear handle on yet.
“I actually use it more as an experimental tool.”
“I’m a TDD native,” she said. “I actually use it more as an experimental tool, where you’re creating a hypothesis with the tests that you then prove or disprove.”
This sense of using tests as a tool to poke around the application design was echoed by other proponents of TDD.
Riley said being forced to adjust tests due to changing designs was just a part of the nature of programming and not a unique failing of TDD. After all, even if tests are written after the code, tests would still have to be adjusted if the architecture shifts.
“That is a natural part of programming because you just don’t know everything upfront,” Riley said. “As you learn more about the problem and the customer’s needs, you realize, ‘Oh, actually, this assumption was incorrect.’ And so that might require certain changes.”
How to Avoid Brittle Tests
But sometimes, developers using TDD may find that tests are breaking so often it hinders their ability to write new code. That may be a sign the tests are not written optimally.
Riley recommended developers pay attention to how test doubles are used in unit tests because they could be causing some brittle tests.
Conrad Benham, principal consultant at Stride, said he avoids creating mocks of domain objects for that reason. If any logic in other parts of the application changes, it could affect tests referencing those objects and soon developers may have to adjust failing tests all over the application.
Riley said there are several approaches that can help developers write more robust tests when using TDD. Some write a few broad end-to-end tests first to help guide their code in the right direction, instead of jumping directly into writing unit tests.
“That’s the one that tends to not change because it’s defining things at a very high level,” he said. “Then from there, you dive into the details of it — you’re writing a whole bunch of unit tests where you’re doing the refactoring steps.”
TDD Can Help Developers Root Out Their Own Biases
Some developers who don’t enjoy TDD talk about how the process feels very prescribed: Tests have to be written before the code, tests must run and fail first, tests have to be deleted and rewritten every time a design change breaks old tests. It can feel like a lot of work without seeing any real benefit.
But strangely, that’s also how TDD proponents describe the coding process when TDD isn’t used, especially when it comes to testing — developers know they have to write tests, so they end up writing tests that can easily pass to get code coverage. It’s easy to fall into that pattern of writing tests for the wrong reasons if tests are written after the code.
“It’s a little bit of a tautological test, in a way,” said Aaron Foster Breilyn, lead consulting software developer at Stride. “You’re reaffirming functionality that you know is there and you built, so you have confidence in [it] — but that confidence isn’t real, it’s falsely held up by the tests.”
“That confidence isn’t real, it’s falsely held up by the tests.”
Instead of finding unintended consequences in the code developers wouldn’t have thought of while coding, writing tests after the fact could actually bake in the developer’s biases and preconceived ideas of how the application is going to work, Breilyn said. Developers could end up neglecting to test their code in important ways because tests are catering to the code and therefore guaranteed to pass.
Freed felt the same way.
“For the places that only do test coverage after the fact, you just find so many tests that don’t actually ever fail, no matter what you change in the code,” she said. “I find that so infuriating.”
She appreciates how TDD helps double-check that her code is doing what she thinks it is doing, helping her find the biases within her own code.
TDD Can Give Developers the Confidence to Take Coding Risks
Developers who dislike TDD may think of the practice as a constricting activity, but proponents of TDD actually feel the opposite — they find the practice quite freeing. Benham compared TDD to the catches on the bottom of roller-coaster cars, which allow cars to fly down hills but ensure they don’t fly off entirely.
“If the roller coaster detaches from the wire, it won’t fall back to the bottom, it’ll just hold,” he said. “I think of tests as that — they allow you to do a small incremental work and verify that you’re not going to go backwards — and if you do go backwards, you’re not going to go backwards very far.”
Benham did try working on an application without following TDD on a small project last year but ended up quickly regretting it.
“I got to this point where I was starting to lose trust in the code that I’ve written,” Benham said. “As I was writing more code, I thought, ‘Have I regressed the old functionality?’ And I was just like, I can’t do this.”
Only after he turned around and retrofitted his code with tests did he feel confident enough in his code to keep developing the rest of the application.
“What actually is producing the value is the set of engineers who practice test-driven development.”
For its supporters, one of the most obvious benefits of TDD — having tests — is actually just a beneficial side effect rather than the goal of TDD. Instead, it’s a tool that can help developers discover their own biases, root out mistakes in their thinking and help keep them close to the roller-coaster track so developers can fly free when they are developing.
And a lot of those benefits depend on the developer. TDD isn’t a magic method, so it’s only as effective as the developer who wields it.
“I don’t think TDD is actually the thing that’s producing that value,” Solomon said. “What actually is producing the value is the set of engineers who practice test-driven development, care about continuous improvement and are always looking for ways to improve both how they work and the systems they’re working on — and they’re using test-driven development to do those things.”
That’s simply not possible if the developer isn’t comfortable with using TDD or hasn’t found a flavor of TDD that is helpful to them, so companies shouldn’t be forcing the practice on their developers.
For Freed, TDD let her take risks with coding and with making impactful design decisions, knowing that her tests will still catch her mistakes if she slips up.
“You are given this total confidence to make significant changes to your design,” Freed said. “I don’t know how to write code that I would actually send out to the internet that didn’t have reliable tests on it. It would be very scary for me — so it just seems like a necessary practice no matter what step you’re at.”