1

The question is not very expressive, anyway what I was wondering is if, if I follow the red-green-refactor cycle for each method is still TDD or if it has to be done globally.
I mean, let's say I have 5 methods to test, I can follow two roads:

  1. Write tests for all methods, start test routine, add code to methods, add requirements to test, re-add code to methods, and so on (simplified, I know).
  2. Write test for a method and follow that cycle for that method until everything is satisfied, then move on to the next method's test and perform the cycle again, and do this for 5 times

Number 1 is TDD, but is number 2 still TDD? I would say yes because it follows the philosophy of TDD, but I'm not sure if it's really like that or I have to write all the tests first and then cycle "globally".

  • @Laiv I'm learning and I want to be sure of what the correct methodology is, and in this case if there's difference between the two approaches. – TodorokiM Mar 22 at 8:13
  • 1
    #2 is very much the idea of tests driving the design. Every new method derives from a previous design decision which has been already tested (and is testable of course) – Laiv Mar 22 at 8:14
  • 8
    1 is not TDD; when test-driving development you write a test, you should only have one failing test (per level, you might have one overarching acceptance test that's failing at the same time as one integration test and one unit test, for example). See e.g. stackoverflow.com/q/1174906/3001761, softwareengineering.stackexchange.com/q/315418/110531. – jonrsharpe Mar 22 at 8:23
  • 1
    @jonrsharpe en.wikipedia.org/wiki/… For the record, you can test on a per-class basis, not necessarily one method at a time. – Neil Mar 22 at 9:03
  • 2
    @Neil you can do it per class, but even classes start by one single method (the constructor). Just replace "method" or "class" by "refactor". Each iteration or refactor leads to new code that is built upon tested and testable code. – Laiv Mar 22 at 9:13
4

Let's take a take step back. What does TDD mean?

First and foremost: TDD has nothing to do with testing. It's not about the tests. The fact that you end up with an automated test suite with 100% coverage is a side-effect. It is a very important and very nice side-effect, but it is not what TDD is about.

The most important part about TDD is the middle "D": "Driven".

It means that the Tests "drive" everything. The tests tell you what to implement. The tests tell you when to stop implementing it. The tests tell you how to implement it. The tests tell you what to implement next. The tests drive the design. The tests drive the development.

On a very fine-grained level, what does that look like?

  1. Verify that all tests currently pass.
  2. Write the simplest test that could possibly fail. Compilation failures, link failures, type errors, etc. are also failures.
  3. If the test passes, you are already done. This can happen if the feature you are implementing is actually a subset of a more general feature that already exists. It can also happen if you failed to follow TDD strictly and wrote more code than what was necessary to make the tests pass.
  4. Verify that the test is the only one that fails.
  5. Verify that the test fails for the right reasons.
  6. Write the simplest code that could possibly change the failure message.
  7. Repeat #6 until the test passes.
  8. Verify that the test passes for the right reasons.
  9. Refactor mercilessly.
  10. Repeat #9 until there is nothing to improve.
  11. Start again from #1 until all requirements have been implemented.

Note that I didn't use the term "unit test" anywhere. In classic TDD, you use a technique which Kent Beck calls Zoom In, Zoom Out, i.e. you will frequently change your viewpoint and look at the system from different "heights", i.e. at different levels of abstraction and granularity. So, some of the tests may end up being integration tests, some functional tests, some acceptance tests, some unit tests. Higher-level tests may overlap with lower-level tests. Often, higher-level tests are slower than lower-level tests, so the duplication makes sense, because the higher-level tests give you more confidence at the system level whereas the lower-level tests give you quicker feedback. However, Kent Beck says that if e.g. an integration test turns out to be very fast, he will simply delete the unit tests that are subsumed by this integration test.

In ATDD (Acceptance Test Driven Development), this working at different levels is a little bit more explicit: you will have another TDD cycle at a higher-level and nest the above cycle inside it. The "outer" cycle then looks like this:

  1. Verify that all tests currently pass.
  2. Write the simplest acceptance test that could possibly fail.
  3. If the test passes, you are already done. This can happen if the feature you are implementing is actually a subset of a more general feature that already exists.
  4. Verify that the test is the only one that fails.
  5. Verify that the test fails for the right reasons.
  6. Perform the cycle described above. The only difference being that in step #11 you don't keep going until all requirements have been implemented, but only until the acceptance test passes.
  7. Repeat #6 until the acceptance test passes.
  8. Verify that the test passes for the right reasons.
  9. Start again from #1 until all requirements have been implemented.

Note, however, that ATDD not fundamentally different from TDD. In TDD, you also write acceptance tests and you also zoom in and out looking at different levels of abstraction. ATDD simply gives it a name and some structure.

There is another methodology that is closely related to TDD. In fact, it is actually the same thing: Behavior Driven Development (BDD). BDD was invented by Dan North when he was teaching TDD, and he realized the biggest hurdle the participants of his courses had, really grasping TDD, was that they did not understand that TDD is not about testing. And the biggest reason why it is so hard to understand that it is not about testing is that all the terminology is about testing.

So, he changed the terminology. You don't write tests, you write examples. You don't assert facts, you expect behavior. Your tests don't form a test suite, your examples form a Specification by Example.

Another thing that BDD does explicitly, which is usually not talked about in TDD (but it is in ATDD), is integrating the idea of the Ubiquitous Language from Domain Driven Design (DDD) into the domain of testing specification. The idea of the Ubiquitous Language is strikingly simple: all stakeholders in the project should have a shared language, and they should use this language for everything in the project. So, if your domain experts call something a "Frobnicator", then the domain object which represents this concept in your code, should ideally also be called Frobnicator.

In the best case scenario, this means that e.g. your business analyst or your customer will be able to read your acceptance tests, and you can actually use the acceptance tests to establish agreement about what a feature is supposed to be, what it means for the feature to be "done". There are even frameworks (the first one was probably Cucumber) and languages (most notably Gherkin) designed specifically for this.

Note that this is the only legitimate reason to have multiple failing tests at once: if you write the tests together with your business analyst or customer, then you cannot really expect them to pair with you the entire time. Rather, you'd meet with them once per sprint and write the acceptance tests for that sprint all at once.

In fact, many acceptance testing framework have a third state explicitly for this purpose: in addition to "failing" and "passing", they also have "pending". (Test is expected to fail because the feature has not been implemented yet. So, a failing pending test lets the suite pass, whereas a passing pending test is actually a test failure!) Also, in Cucumber-like frameworks, the tests are split into two parts: the high-level "Feature" description that is layman-readable in a business-oriented high-level domain specific language, and the "Step Definitions" that are developer-oriented and written in the underlying test framework's language. Features whose Step Definitions are missing are automatically marked as "undefined" which will also not fail the test suite (it is in fact "yellow", so neither red nor green).

But again, note that BDD is not fundamentally different from TDD. It just uses different terminology to make it easier to understand what TDD is about. Dan North himself has said that if you do TDD "right", then you are already doing BDD. BDD just helps you to do TDD "right".

So, let's get back to your concrete examples.

Your Number 1 is not TDD. You will have multiple failing tests at least some of the time, which is generally not allowed in TDD. (I mentioned an acceptable exception above: if you write your tests together with the customer, and the customer doesn't have time to sit with you all day, then it is acceptable to write a bunch of acceptance tests in one go.)

However, there is an even more important reason why it is not TDD, and it is in fact the same reason why Number 2 is not TDD as well.

You write:

Write test for a method

That is not TDD. In TDD, the tests drive the development. The tests tell you what to write. You cannot know and in fact you must not know beforehand whether you will be writing a method, two methods, some methods, an object, a group of objects, or just a part of a method. The tests tell you that. If you already know that you are writing a test for method, this means that you have already "written" at least a tiny part of the code, even if it is just in your head. If you have already written some code, even if it is just informally in your head, then you are not writing the tests first, and you are not letting the tests drive the development.

This is hard. Very hard.

Keith Braithwaite has created an exercise he calls TDD As If You Meant It, that is meant to help with this. It consists of a set of rules (based on Uncle Bob Martin's Three Rules of TDD, but much stricter) that you must strictly follow and that are designed to steer you towards applying TDD more rigorously. It works best with pair programming (so that your pair can make sure you are not breaking the rules) and an instructor.

The rules are:

  1. Write exactly one new test, the smallest test you can that seems to point in the direction of a solution
  2. See it fail; compilation failures count as failures
  3. Make the test from (1) pass by writing the least implementation code you can in the test method.
  4. Refactor to remove duplication, and otherwise as required to improve the design. Be strict about using these moves:
    1. you want a new method—wait until refactoring time, then … create new (non-test) methods by doing one of these, and in no other way:
      • preferred: do Extract Method on implementation code created as per (3) to create a new method in the test class, or
      • if you must: move implementation code as per (3) into an existing implementation method
    2. you want a new class—wait until refactoring time, then … create non-test classes to provide a destination for a Move Method and for no other reason
    3. populate implementation classes with methods by doing Move Method, and no other way

What this exercise does, is it places further restrictions steps #6 and #9 of the "inner" TDD cycle, the first one I mentioned above. In step #6, you write all the implementation code directly in the test. And in step #9, you move the implementation code outside of the test method by (ideally) only one of these two mechanisms:

  1. You are only allowed to create new methods as part of an Extract Method Refactoring where you extract the implementation code out of the test method into an implementation method (but still within the test class). Also, this is the highly preferred mechanism to extract implementation code out of a test method, you should try to avoid at all cost to add the code to an existing method.
  2. You are only allowed to create new classes to serve as the target of a Move Method Refactoring. This implies that the class needs to be created empty, and you never write any code in the class, you only move methods into it that already exist.

This is not intended to be a way for writing production code, it is intended as an exercise. But, you never know, try it, and maybe you find it makes your code better!

You can read about it more in these questions:

And there's some additional interesting information in these articles:

6

Kent Beck, in Test Driven Development with Examples, writes:

Remember, the cycle is as follows

  • Add a little test
  • Run all tests and fail
  • Make a little change
  • Run all tests and succeed
  • Refactor to remove duplication

In other words, you are usually working with one failing test at a time.

As long as you are performing the complete ritual for each test, I don't think it particularly matters whether you batch all of the work for a single method together, or if you change focus from one to another.

Have a checklist of tests that you still need to add, sort them, and do whichever test in your list is at the top of the list.

If that means that you change which method you are testing as you move from one test to the next, I think that's fine.

  • "...If that means that you change which method you are testing as you move from one test to the next...". TDD isn't about "which method you are testing", it's about writing failing tests and making them pass. "Testing methods" is effectively a side effect, not the primary goal. But aside from the back to front emphasis there, you get the advice spot on (and you seemed to have understood better what the OP was asking than I did). – David Arno Mar 22 at 11:42
5

I mean, let's say I have 5 methods to test

That struck me as odd, because in my understanding of TDD, you never ever have 5 methods to test (at once). You have either:

  • No method at all

Or

  • One method which currently works, but where you want to change the API or the behaviour of that method

Then you write a test - which fails, either because it does not compile, or because the method currently does not have the intended behaviour. And then you modify the code until the test succeeds (and afterwards refactor).

If that is what you meant under #2, then it is TDD. But #1 has nothing to do with TDD, quite the opposite.

1

As I understand what you are describing in your examples 1 and 2, neither is TDD in its strict sense.

TDD is simply, write a failing test, write some code to make the test pass without breaking any other tests, refactor without breaking any tests and then repeat. There's no concept of classes, methods etc in there, so don't try to think in those terms.

Of course, TDD in its strict sense doesn't always work well. So if you find yourself doing a more "test orientation development" approach of writing a few tests, then getting them all to pass, or writing some code before the tests, don't sweat it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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