Hacker News new | ask | show | jobs
by kazinator 1386 days ago
You can't "drive the design" with TDD. To write a failing test, you have to have a requirement which is encoded by the test. That requirement doesn't come from TDD, and TDD can't tell you how to procure it.

To go from a high level requirement like "command line application to track transactions and show expenses by category", you need to go through several levels of detailed design before you have anything that is implementable by a test-driven approach, which will necessarily be bottom-up.

Remember that tests can only confirm the presence of a defect, not its absence, and that not everything can be tested due to the intractability of exhaustive testing.

By blindly following test-driven design, you could end up with a useless, inflexible program that correctly categorizes expenses over only a small, fixed vocabulary of categories because it never occurred to anyone that a category must be something user-defined.

A user-defined category feature cannot be implemented with strict TDD because the space of user-defined categories is infinite. Say you have a test which confirms operation for every valid category string up to three characters long. That doesn't prove the system will not fail for a four-character-long category.

The only people who regard TDD as some kind of panacea are those with no formal background of any kind, who don't understand the role of testing in the overall software engineering context, and what its limitations are.

3 comments

That's very false in my experience, and I did 100% TDD for years (have a more nuanced approach currently in TypeScript/React)

As a programmer with a problem, you first instinct is to start at the solution.

TDD pulls you back, and you first have to write the api and decide how you verify it.

If you're doing it well, you make both of those things as simple as possible, first. Reduce dependencies and inputs, etc.

That's actually the most important part, an easily consumable api that's easy to verify, not the implementation, and TDD forces you to do it first.

Also, a pet peeve of mine is seeing a line or more of code that doesn't do anything. With TDD, such detritus is impossible as you can't write production code unless it's making a red test turn green.

Even a string length api like len(s) is not exhaustively testable; you can't prove it correct with blackbox tests.

TDD has refactoring steps which only have to preserve passing tests; refactoring can easly be the vector that introduces dead code as well as changes behavior for untested input combinations.

I suspect that a lot of code developed TDD is actually deployed on input combinations that are not covered in the TDD test suite.

A string length function developed by TDD will still work on len("supercalifragilisticexpealidocious") even though that exact string never appeared as a test case, and the consumers of that function will use it on all sorts of untested inputs all the time.

> Even a string length api like len(s) is not exhaustively testable;

But this has nothing do with what GP said. That isn't what gp is using TDD for. GP is using TDD for,

1. starting with api of a function vs code.

2. avoiding unused lines of code

3. reducing dependencies, its hard to tdd something that has a lots of deps so it drives your refactor it something more testable and thus more readable/maintainable also.

4. simplest set of input/output.

A very good explanation. Yet what I think is confusing a lot of (very?) good programmers (not me) is that they already do that intuitively by not letting methods become longer than 3 or 4 lines, and whenever implementing a new method, by writing the return statement first (i.e. returning a mock object or similar) and working backwards, i.e. only writing enough code to be able to return the desired value.

If one then goes and tries to write tests for these kind of methods, it feels like superfluous busywork.

That's a good thing, TDD is a prompt to say you don't really understand this requirement. Requirements aren't a design.

TDD makes you define what your input is, and what your output is. What's in the middle your free to choose.

Not necessarily. There are two schools of thought, Outside In TDD and Inside Out.