RED - Write a Test that Fails

Một phần của tài liệu Practical unit testing with JUnit and mockito (Trang 58 - 61)

Think about some functionality that should be implemented and write it down in the form of a test. This functionality is not yet implemented, so the test will inevitably fail. That is okay. Now you know that:

• the functionality really does not work,

• once it is implemented, you will see it, because the test result will turn from red to green.

At first you might feel awkward when writing tests for something which is not even there. It requires a slight change to your coding habits, but after some time you will come to see it as a great design opportunity. By writing tests first, you have a chance of creating an API that is convenient for a client to use. Your test is the first client of this newly born API. This is what TDD is really all about: the design of an API.

When thinking like a client of your own soon-to-be-written code, you should concentrate on what is really required. You need to ask questions like: "do I really need this getter that returns this collection, or would it be more convenient to have a method which returns the biggest element of this collection?".

And you need to answer such questions by writing tests. This book is not devoted to API design, but even if we were to just scratch the surface of this topic, it could entail a revolutionary change in your coding style. No more unnecessary methods, written because "they might just possibly be useful for someone someday", no more auto-generated getters/setters, when an immutable class is much more appropriate.

Concentrate on what the client (the test code) really needs. And write tests which test exactly this, and nothing more.

Writing a test first will make you think in terms of the API of the object. You won’t yet know what the implementation is going to look like (even if you have some vision of it). This is good: it means your tests

Chapter 4. Test Driven Development

will have more chances of testing the external behaviour of the object, and not its implementation details.

This results in much more maintainable tests, which are not inseparably linked to the implementation.

Always start with the failing test, and always observe it failing. Do not neglect this step! It is one of the few ways to learn about the quality of your test (see Chapter 11, Test Quality).

Of course, you cannot run the test right after having written it. Why? Because if you truly followed the

"never write code without a failing test" rule, then your test would be using some non-existent classes and methods. It simply would not be able to compile. So, as part of this first step you will also need to make the test compile. Usually IDE will make this a breeze, by creating a default (empty) implementation of classes and methods which are used from the test code.

In reality, writing a failing test might sometimes be more trouble than it is worth. Please refer to Section 4.9 for a discussion.

How To Choose the Next Test To Write

Telling you to simply go and write a failing test "just like that" is kind of unfair. It sounds easy, but how should one go about it? Say we have a list of functionalities to be implemented, and a list of tests which cover them. The question we have to deal with right now is how to choose the first test. And then, after you implement it, and finish the TDD circle by implementing the code and refactoring, how to go about the next test? And the next one?

This is a standard problem and, as far as I know, no "standard" answer exists. There is no heuristic that is commonly recognized as the right way to determine the next test to be implemented. However, there are some tips shared amongst the community that might be helpful. Let us have a look at them.

The Low-Hanging Fruit. This rule says: "Start with something really simple. Implement an obvious test case."

This technique is especially useful if you are stuck. Writing something, even something trivial or of only minimal importance, might be helpful to overcome this sort of "writer’s block". When you have doubts about what tests to write and how the tested method should behave, then making the first step might be the best thing you can do. Even if the functionality implemented in this way is not so important, you will at least get some of the pieces of the puzzle (the SUT class and some of its methods) in place. It might help you to move forward.

An example of writing a simple test case just to get you started would be:

• writing a parameter-checking test for a function (no matter what the purpose of the function in question might be),

• or, when writing a parser, starting with the test case of passing an empty String to the parsing method and receiving null in return3.

Chapter 4. Test Driven Development

insight into the real task ahead. This could be really handy, if you are feeling lost and do not know how to proceed further.

The Most Informative One. Another approach is to start with the test which gives you the most information about the implemented functionality. This is like striking the ball with the sweet spot: it yields the maximum possible return in terms of knowledge.

However, this usually means facing up to the most difficult dilemmas. Well, you are going to have to deal with them anyway, so maybe instead of circling around, why not simply jump right into the action?

This approach is like saying "it does not matter that my first match is against the world champion - if I am going to win the whole tournament, I will have to beat him anyway". Some people like this kind of motivation.

All good and well, but haven’t we just answered a riddle with another riddle? The question, now, is how to know which test will furnish you with the most knowledge about the implemented functionality?

Well, this is not so hard to answer. This is probably the test which you know you still do not know how to make pass. You will simply know which one that is.

In the case of the preceding parser example, had you adopted this approach, then you would probably have started out by parsing a full sentence. This would definitely teach you a lot about the functionality being implemented.

First The Typical Case, Then Corner Cases. It seems quite reasonable to start with a "typical case".

Think about how you would expect this function to be most frequently used. When writing a tokenizer, start with a valid sentence as an input. When implementing a vending machine, begin with a client inserting a $1 coin and selecting a product which the machine has. Later on, you will implement corner cases.

Also, this approach guarantees that you have something valuable working from the very beginning.

Even if you get dragged away from your computer (e.g. to some very urgent meeting of the highest importance) you will have already implemented something which is useful for the clients of the SUT.

That will be nice.

This approach represents a "natural" order of testing, as discussed in (see Section 6.1).

Listen To Your Experience. Probably the most valuable way to deal with the "next test" dilemma is to listen to your experience. It should tell you which one of the above approaches is the most suitable for this particular case. Personally, I like to go with the typical cases first, but it happens that I use other approaches as well.

Readable Assertion Message

After you have a failing test and before you start implementing the code to fix it, it is advisable to take care of one additional detail: make sure that the message printed by the failing test indicates precisely what is wrong. Sometimes, in fact quite often, the default information printed by JUnit is good enough.

If not, work on the error message (see Section 8.4 for some hints) until you are satisfied with the result.

Then, move on to the next step.

Chapter 4. Test Driven Development

Một phần của tài liệu Practical unit testing with JUnit and mockito (Trang 58 - 61)

Tải bản đầy đủ (PDF)

(310 trang)