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?
- Verify that all tests currently pass.
- Write the simplest test that could possibly fail. Compilation failures, link failures, type errors, etc. are also failures.
- 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.
- Verify that the test is the only one that fails.
- Verify that the test fails for the right reasons.
- Write the simplest code that could possibly change the failure message.
- Repeat #6 until the test passes.
- Verify that the test passes for the right reasons.
- Refactor mercilessly.
- Repeat #9 until there is nothing to improve.
- 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:
- Verify that all tests currently pass.
- Write the simplest acceptance test that could possibly fail.
- 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.
- Verify that the test is the only one that fails.
- Verify that the test fails for the right reasons.
- 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.
- Repeat #6 until the acceptance test passes.
- Verify that the test passes for the right reasons.
- 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:
- Write exactly one new test, the smallest test you can that seems to point in the direction of a solution
- See it fail; compilation failures count as failures
- Make the test from (1) pass by writing the least implementation code you can in the test method.
- Refactor to remove duplication, and otherwise as required to improve the design. Be strict about using these moves:
- 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
- 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
- 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:
- 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.
- 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: