The problem with test-driven development is that it focuses attention on getting specific features working, rather than finding the best design. This is tactical [as opposed to strategic] programming pure and simple, with all of its disadvantages. Test-driven development is too incremental: at any point in time, it’s tempting to just hack in the next feature to make the next test pass. There’s no obvious time to design, so it’s easy to end up with a mess.
One place where it makes sense to write the tests first is when fixing bugs. Before fixing a bug, write a unit test that fails because of the bug. Then fix the bug and make sure that the unit test now passes. This is the best way to make sure you really have fixed the bug. If you fix the bug before writing the test, it’s possible that the new unit test doesn’t actually trigger the bug, in which case it won’t tell you whether you really fixed the problem.
Design time is the refactoring after your tests pass.
That's when you can move your code around with confidence, since you can lean on your tests.
It's hard to overstate how common this is.
If your design mistakes are in your code and your tests, they'll also likely be in your mental model and your documentation (if it exists). At which point you what? Throw it all away and start over? In extreme cases, but in others you do partial rewrites and refactors. Address the issues.
But not having tests because they may encode your design mistakes is as foolish as not having documentation or comments because they may also encode your design mistakes. In a few years, you'll have a blob of code that, in the best case, is perfectly readable and comprehensible. But, in reality, is likely to fail at communicating the overall design intent and requirements.
And so what if it doubles the cost of fixing the mistake? You made a mistake and it had to be fixed anyways. In the end, I've never found tests to cost more than they saved. A lack of tests has always led to higher development costs (primarily as measured in time to delivery). Regressions are ludicrously common without tests, which eats away at your time (and therefore adds to your costs) very quickly.
Kinda, yeah. Or huge chunks of it. Thats what the OP said. I've done a lot of that.
Was that a rhetorical question?
>And so what if it doubles the cost of fixing the mistake? You made a mistake and it had to be fixed anyways.
Well, then assuming you need 1 month to do a product iteration and 1 to do tests and docs on it and you need 5 iterations before landing on the "right" requirements to meet product-market fit, that requires 10 months to get to fully tested code doing the right thing with TDD and 6 without.
What if your runway is 8?
>In a few years, you'll have a blob of code that, in the best case, is perfectly readable and comprehensible. But, in reality, is likely to fail at communicating the overall design intent and requirements.
I've been on both sides of this problem and I honestly think that this is much less dangerous than not iterating on requirements fast enough.
I've bailed a company out of a massive technical debt hangover before. It's horrendous but it's not usually fatal. But solving the wrong problem? Not finding the right problem soon enough? That's usually fatal.
What you are describing is not refactoring: "In computer programming and software design, code refactoring is the process of restructuring existing computer code—changing the factoring—without changing its external behavior."[0] The interface is an integral part of external behavior.
With that said, what is "internal" vs. "external" behavior is… fluid. A large-scale refactoring might involve changes to internal components with their own interfaces and tests. The tests for the component being refactored should not be affected, but in the course of refactoring the larger component you might make more extensive changes—not just refactoring—to the smaller pieces making up its internal architecture. For that you would need to design new interfaces & requirements for the internal components, write new tests, and then iterate on the implementation until the tests pass, just as with any other TDD process. In the meantime you have the unmodified tests for the larger component to verify that your new internal architecture still satisfies those higher-level requirements.
Sure, in with bigger refactorings, some unit tests probably change. That's just a (small) part of everyday work, as I do it.
But it just goes to show any technique (including TDD) is not going to force you into good habits. This needs to be learned with experience.
Then when they're at the top they don't know what better would have looked like ;)
They get to be happy the whole time though.
Adjacent parts of our company were working on a Heroku clone PaaS, and I got some time on a team building an auto scaler service to spin up and down other services. We didn’t know how our service would kill and start other services. Tests were changing as much as production code while we worked on that. A lot of our work was spiking to see how our service fit into the ecosystem of other services.
Dogma is the bain of everyone :) I will never advocate "100% TDD." It is a tool like any other that has its place and, more importantly, doesn't fit everywhere. Though I will freely admit I do encourage people to use it more than what I have seen to be the typical amount.
> When it came to adding a feature to a Rails app, the interface is pretty well defined, it’s going to serve up HTML and js, it’s got to fit on the existing structure of the page or conform to a design.
It's very unlikely I'd use TDD on that. Boilerplate/plumbing is something I rarely use/advocate TDD for - it's the "clever" stuff you want to cover. The stuff that is following rules outside of the code itself.
> Tests were changing as much as production code while we worked on that.
Absolutely nothing wrong with that, imo.
When your requirements change, and you change your code, you have two questions. First, did my code change do what is needed for the requirements change? You modify tests (or write new ones) to answer that.
Second, did I break anything in the process? The remaining tests answer that.
Don't productionise prototypes.
But that should be part of the "production-izing" process, right? You convert it to a solid design instead of a quick-and-dirty hack?
The whole point was getting to a design that fulfills the feature requirements but for which the contract boundaries weren't known going in.
It's frequently scorned but Im not so sure it's always the worst idea in the world.
I've built shoddy hacked together systems that made customers happy and wasted months building heavily tested well structured code doing something nobody wants.
Well engineered tests take a lot of time to build - sometimes 2x the code itself. If you've built the wrong thing youve paid 3x the cost to figure it out before going back to the drawing board.
I'm a big believer in retrofitting tests once code has proven itself useful.
Why? Because if I find it hard to write the test, it's telling me that I'm likely to find it hard to write non-test code that uses the class/module/subsystem/whatever. It forces you to use the interface to your code. If it's hard to use, that's telling you to consider changing the design.
No design gets to the coding stage until someone has answered "how will this be tested?"
So, yeah, you should absolutely be designing around tests.
The design spec documents determine what the implementation should look like, and the tests should verify the implementation works as desired, not drive its structure.
I don't want the implementation to be chopped up and scattered everywhere in the name of "testability" because someone decided that a tool should dictate the architecture of the code. That leads to unreadable, hard-to-maintain, inefficient code that, with a tight coupling to the test suite that makes both them of fragile in the presence of changes in the other. For very little gain.
I completely disagree with everything above. Literally, take the sentence and make almost every word the opposite.
> Requirements drive your tests, which drive your code.
It's verification of implementation, not a unit testing, so it's a VDD, not a TDD.
It's in the name.
Sure, just writing tests is entirely about automation, but TDD is more than just writing tests.
Originally, it's Test Driven Development.
I see no connection between TDD and (good) design, but other folks see.