2

I admire Bob Martin's Clean Code. Lately, however, I realized an apparent contradiction.

One of the three rules of TDD, which Bob Martin advocates for in his book, is to never write more code that is necessary to get your tests pass

But that strongly assumes each app requirement is translatable into a test assertion which isn't the case

Take clean code, for instance. Tests don't care how many things your functions do, how many things your classes do. Refactoring (cleaning) code often involves writing more code – code that is not necessary for your tests. It could be extracting a smaller function or extracting a whole new class. Strictly speaking, even renaming a variable or method is writing more code if the new name is longer (even if it makes the code more readable and cleaner)

So at the end of the day, you have to either

  • ditch TDD
  • ditch every non-testable requirement (such as clean code)

Since TDD is a PITA anyway, the choice seems to be an easy one

Am I wrong and the two can coexist?

2
  • 4
    Two remarks beyond Greg's observation that TDD implies a refactoring phase: (1) Robert C Martin has opinions, and many of them aren't that good. Clean code is a great marketing term, but Martin doesn't do a great job of explaining when good is good enough. By explaining concepts via teachable slogans, nuance is lost. (2) You can make a lot of aspects automatically checkable via linters and static analysis, for example variable naming schemes or a method's cyclomatic complexity. That's not a test in the TDD sense, but has equivalent developer experience.
    – amon
    Commented Jul 27 at 13:47
  • When people talk about TDD most of the time they think about testing runtime behaviour. However it most certainly is possible to test the code design itself up to a certain level. That's what static analyzers do all the time. However to fully enforce things like clean design is simply not possible technically. Such static analysis is just too hard I think. For example: how can I write a test that checks that a class does only one thing? That's not even formally well defined.
    – freakish
    Commented Jul 27 at 16:11

3 Answers 3

11

This is the challenge with taking the things people write too literally. In a very strict sense of the word, as soon as you have a passing test, you can stop writing code. That could include renaming variables or moving code into a private function. I don't think that is keeping with the spirit of what Uncle Bob is trying to say, and that certainly isn't following the spirit of test driven development.

Remember the basic rhythm of TDD (red, green refactor):

  1. Write a test (“red”).
  2. Get the test to pass (“green”).
  3. Optimize the design (“refactor”).

Source: The TDD Cycle: Red-Green-Refactor, "Modern C++ Programming with Test-Driven Development," O’Reilly

That implies you can clean code up. That implies that you should rename that variable to make it clearer. You should move that logic into a private function because you have tests to verify the requirements that have already been built. You could even move a cohesive set of logic into its own class, and you are following TDD and clean code!

The key to rectifying TDD with clean code is to understand that they focus on different aspects of a system. TDD focuses on behavior and structure. Clean code focuses how easy a human can reason about the code. Test driven development enables you to write clean code, because TDD provides a test suite you can execute to verify that you have not introduced defects while cleaning the code up. TDD, to my knowledge, isn't concerned with how an application looks to an end user.

Visual testing is more appropriate to verify the look and feel of an application. This tests the application at a higher level of abstraction than most tests written following TDD. TDD can be used to test the GUI of an application, but those tests will be written to isolate the GUI component to verify its expected behavior in code. I haven't seen TDD be applied to how a GUI looks to a human.

If someone tells you to stop writing code as soon as the tests pass, they don't literally mean "take your hands off the keyboard." They mean don't write more behavior than is necessary. Don't implement logic outside the scope of those requirements. This is not an exact description of what code you can write, and what code you cannot. It is general guidance meant to prevent you from building more than is necessary. This is another way of adhering to YAGNI.

11
  • 1
    Thank you. However, even with RGF, TDD makes an assumption that the only non-testable requirement is code cleanness. Which isn't necessarily the case. It could be called "red, green, refactor and do everything else that isn't testable but should be done nonetheless". But it's not what the motto says. So there's still the gap between TDD and meeting all the requirements Commented Jul 27 at 14:05
  • 1
    For one thing, there's no chance in hell you can make a robust test suit for GUI. You will always have to check it with your own eyes. Though, I guess, that is a test in a sense Commented Jul 27 at 14:09
  • 1
    @user1937198, you can test for robustness provided you have specific technical specs to hit. When given a requirement like "make sure it is robust," ask follow up questions to define what robust means in that context. Now you have requirements to test. If they cannot define what robust means, then you decide what it means, and write tests to that effect. Or ignore the robustness requirement completely, because nobody knows what it means, which renders this question moot. Commented Jul 27 at 15:13
  • 5
    @SergeyZolotarev I think you're blindly assuming that one must have one single ideology that must encompass everything that is important. This is not true. For example, TDD does nothing to promote code readability, change-friendliness or any kind of internal implementation design. However, that doesn't mean that TDD is explicitly telling you that those things don't matter, it's just not related to what TDD is talking about. You shouldn't be developing a TDD-only codebase. You should be developing a codebase using TDD principles alongside any other principles that matter to you.
    – Flater
    Commented Jul 28 at 23:41
  • 2
    @user1937198: All test suites should inherently come with a label that says "amend this suite when the future shows you its imperfections". It's not realistic to expect to cover every possible test scenario, nor deterministically prove that you have done so. All you can do is cover it as best as you can think of (time-effort balance), and then if you bump into any gaps later on, patch those up as you go. It's not about never making a mistake; it's about not making the same mistakes a second time (i.e. because after the first time, you should fix your test suite to now be wise to it)
    – Flater
    Commented Jul 28 at 23:45
4

Late to the party. Please forgive me. Got busy today.

How do you square TDD with non-testable requirements?

You don't. TDD is about testing. It's not about what you don't test.

One of the three rules of TDD, which Bob Martin advocates for in his book, is to never write more code that is necessary to get your tests pass

Right, when you're doing TDD. Not every line of code gets developed and covered under TDD. When you write such code, you're on your own. This is why code coverage tools don't just turn from red to green. They keep telling you your coverage percentage. Which, frankly, isn't the important thing to know.

For one thing, there's no chance in hell you can make a robust test suit for GUI. You will always have to check it with your own eyes. Though, I guess, that is a test in a sense – Sergey Zolotarev

Uncle Bob has talked about that problem and presented the solution. It's called the Humble Object. When grappling with hard to test things, like GUIs or IO aware code, hollow them out so they hold no actual logic. Make them so simple that the only real test they need is a code review. Move the interesting code to somewhere easy to test. Throw your TDD at that.

That right there tells you the important thing. The important thing is knowing which code is under test. You can have 99% code coverage but if that 1% is hiding in some critical hard to read logic I'm going to kill myself covering it, moving it, or eliminating it because that's a nightmare I don't need. But even with coverage of 70%, I'll sleep soundly if I know 100% of that 30% is in logic-free, easy-to-read code. Because reading code is a test too.

This humble object pattern has a close cousin. It's called Functional Core, Imperative Shell. Here we explicitly set aside a part of our architecture for the hard-to-read, easy-to-test business logic and another part for the hard-to-test, easy-to-read configuration code that knows where all that IO/GUI crap is. It's really the same idea. Just makes it easier to tell what goes where.*

* Spring made a mint forcing us to do the imperative shell in XML so we had to separate them.

The rules of TDD are for when you're writing code that you want to be sure is covered with your automated tests. And that's covered all the way down into the little private methods. You don't need to call them directly. Just exercise them.

Following the TDD rules lets you know that what you're creating is covered. They never said you can develop 100% of any app this way.** They said follow these rules and what you create while following them will be 100% covered. That can be very useful to know.

** Since the most testable code is functional code (at least, deterministic & side effect free) they’ve actually proven you can’t write 100% of any useful program with only testable code. You need side effects to communicate with the outside world.

But that strongly assumes each app requirement is translatable into a test assertion which isn't the case

TDD is not, has never been, and never will be, the only way to test.

"When the app is running the computer must smell like cinnamon"

OK, run the app. Do you smell cinnamon?

Not everything is TDD. Not everything needs an automated test. But my god don't pretend code shouldn't be tested just because you can write untestable code.

0

How do you square TDD with non-testable requirements?

You don't. TDD it is writing tests before developing the software, it is alternative documentation. TDD it is a flavour of waterfall methodology, the major difference being that documentation is the test suite. What isn't in test doesn't exist. "TDD with non-testable requirements" is an oxymoron, that is just two opposing words, requirements that are apart of requirements, dissimilar to paradox that is "a self-contradictory statement that can actually be true".

Not the answer you're looking for? Browse other questions tagged or ask your own question.