3.1 Step by Step


3.1 Step by Step

"The development progresses in small steps, where testing and coding alternate. Such a 'micro-iteration' does not take more than 10 minutes." We will use this quotation from Chapter 1, Section 1.4 from now on as our guiding principle.

When we begin to develop software, the requirements specification is our primary source of information. Accordingly, for the classes and objects at the outer border of a system, the majority of test cases will somehow be linked to those requirements. To be more precise, the more testable the requirements are, the easier it is to derive concrete and executable tests from them. Typically requirements documents are not as precise as we would like them to be and require some sort of interpretation. The following example is typical in that respect as we will see.

Let's begin with the first micro-iteration and create an empty test class:

 public class DictionaryTest extends TestCase { } 

The name of the Dictionary class is already given so that we get the name of the test class, DictionaryTest, automatically. In many cases it will be sufficient to assign exactly one test class to one application class. This is a good starting point in any event. When starting the test runner with DictionaryTest as a command line parameter, we get the expected failure: No tests found in DictionaryTest. Now let's add the first test:

 public void testCreation() {    Dictionary dict = new Dictionary(); } 

We have to make two decisions to this end: Which parameter is required by the constructor? How should we name the test? Considering that we do not currently have any clue for a parameter, our preliminary decision will be in favor of an empty constructor. The name of our test case—testCreation —is a typical name for a first test.

An attempt to run this test will fail from the outset, that is, the compilation fails. Compilation can be considered as our first testing cycle; successful compilation thus becomes an intermediate step on our way to a green bar.[1] So first the application code:

 public class Dictionary { } 

As you can see, we see nothing. The default constructor does its job, but wonders will never cease! The test runs perfectly. We can now doubtlessly prove that we are capable of creating a Dictionary instance, but nothing else. We would at least like to make sure that a newly created dictionary is empty, so we expand the test

 public void testCreation() {    Dictionary dict = new Dictionary();    assertTrue(dict.isEmpty()); } 

Together with this expansion, we made our first design decision, namely that the interface of the Dictionary class provides the isEmpty() method. Such decisions have to be made constantly during the course of a test-first development. The sum of this large number of small progressions eventually leads to the overall system design, an approach we introduced as evolutionary design in Chapter 1, Section 1.2. With our tests we ensure that our design complies with the implementation; a design drawn on paper does not have this property. In addition, we can use the tests to revise our design decisions and do a refactoring.

Coming back to our example, you may wonder why we chose to test a newly created dictionary for being empty. Why not start by adding a couple of translations to the dictionary? The point is that we always try to test the most basic case first to have a stable point for further evolution. The most basic case we could think of here was an empty dictionary. Moreover, we were confident that we could fulfill this test case rather quickly:

 public class Dictionary {    public boolean isEmpty() {       return false;    } } 

The simplest implementation of a function is always to return a constant value, and simplicity is an important design goal. The corresponding rule from Chapter 1, Section 1.4 states, Write only as much production code as needed for the test.

Continuing with our example, we first select the return value that causes the test to fail, that of "false." Next we run the test to see if it really fails. This step is important for our confidence in the correctness of our test. And, now, we correct the implementation so that it will run successfully:

 public class Dictionary {    public boolean isEmpty() {       return true;    } } 

Although we have a vague inkling that the hardwired return of "true" will not be our final implementation, we obey our previous rule and are guided solely by the tests. We trust that some future test will force us to do the "correct" implementation. Keep that in mind: a premature "correct" implementation will lead to a situation where we would eventually write an insufficient number of tests, because we would naturally leave out all those tests already implemented by the application code. Writing only as much code as is forced by existing automated tests ensures that all code is being executed by those tests. This is a huge step forward on our travel to an adequate set of tests.

We want to take the first step towards a functioning dictionary in the next test:

 public void testAddTranslation() {    Dictionary dict = new Dictionary();    dict.addTranslation("Buch", "book");    assertFalse(dict.isEmpty()); } 

This test looks a little meager since it does nothing but check whether or not a dictionary is no longer empty after adding a translation. Presumably, you expected bigger progress, for example, adding several translations and polling them. By the way, as the name suggests, assertFalse(...) is JUnit's counterpart to assertTrue(...) and checks that the passed predicate is evaluated to false.

Remember that it is our objective to work in small steps to make sure we will never experience a sudden and unexpected test success or test failure. In practice, most developers take increasingly bigger steps, with the result of frequent surprises when they run their tests. That's where we will hopefully distinguish ourselves as test-first masters by making giant steps when moving in well known territory and making very tiny forward and sideward steps in unknown terrain and on slippery ground. However, this strategy requires that we know how to make the tiny steps in the first place.

You can now (and in the future) do the intermediate "empty" implementation leading to a failed test without any assistance. For this reason, let's make an attempt to meet the test requirements:

 public class Dictionary {    private boolean empty = true;    public boolean isEmpty() {       return empty;    }    public void addTranslation(String german,                               String translated) {       empty = false;    } } 

The implementation is simple and unexpected; after all, the given German word and its translation are not used at all. This points to a massive gap in our test, which we will try to close as follows:

 public void testAddTranslation() {    Dictionary dict = new Dictionary();    dict.addTranslation("Buch", "book");    assertFalse(dict.isEmpty());    String trans = dict.getTranslation("Buch");    assertEquals("book", trans); } 

For the first time, this looks like a test that really does something useful, namely find and check the translation of a word. But if you had hoped to eventually see the final implementation code, you were mistaken. Adding the following method will perfectly satisfy the test requirements:

 public class Dictionary {    public String getTranslation(String german) {       return "book";    } } 

It looks like we are unable to get out of doing another test case. This test case should force us to give up on the unsatisfactory return of constants. How about this:

 public void testAddTwoTranslations() {    Dictionary dict = new Dictionary();    dict.addTranslation("Buch", "book");    dict.addTranslation("Auto", "car");    assertFalse(dict.isEmpty());    assertEquals("book", dict.getTranslation("Buch"));    assertEquals("car", dict.getTranslation("Auto")); } 

Now look at the following. Even a malicious developer[2] wouldn't be able to come up with a simple implementation that won't move in the expected direction slowly but surely:

 import java.util.Map; import java.util.HashMap; public class Dictionary {    private Map translations = new HashMap();    public void addTranslation(String german,                               String translated) {       translations.put(german, translated);    }    public String getTranslation(String german) {       return (String) translations.get(german);    }    public boolean isEmpty() {       return translations.isEmpty();    } } 

A Map serves us now to store translations; in turn, the variable "empty" landed in the waste bin. Once again, the tests ensured that we didn't forget the isEmpty() method while restructuring. The use of the hash map is definitely easier, compared to the solution drafted preliminarily in the introduction (see Chapter 1, Section 1.3). In case the introduction of a Translation class becomes necessary later on, the existing tests will protect the behavior achieved so far. But let's not speculate.

Now that we have the correct functionality built in, it is recommended to have a look at the test class to clean it up or do a refactoring. Several things leap to the eye:

  • The names testAddTranslation and testAddTwoTranslations no longer reflect the entire contents of the test cases. The names testOneTranslation and testTwoTranslations look much better.

  • The line that creates our Dictionary test object occurs several times. We turn this into a test fixture.

  • The second and third test cases contain more than one assert. To facilitate future diagnostics, it is recommended to mark the individual assert calls by a descriptive comment as the first parameter. This variant is supported by all assert methods.

The freshly styled DictionaryTest class now looks like this:

 public class DictionaryTest extends TestCase {    private Dictionary dict;    protected void setUp() {       dict = new Dictionary();    }    public void testCreation() {       assertTrue(dict.isEmpty());    }    public void testOneTranslation() {       dict.addTranslation("Buch", "book");       assertFalse("dict not empty", dict.isEmpty());       String trans = dict.getTranslation("Buch");       assertEquals("translation Buch", "book", trans);    }    public void testTwoTranslations() {       dict.addTranslation("Buch", "book");       dict.addTranslation("Auto", "car");       assertFalse("dict not empty", dict.isEmpty());       assertEquals("translation Buch", "book",                     dict.getTranslation("Buch"));       assertEquals("translation Auto", "car",                     dict.getTranslation("Auto"));    } } 

Comparing our set of tests with the specification reveals a yet unimplemented functionality: German words with more than one possible translation. First the test:

 public void testTranslationWithTwoEntries() {    dict.addTranslation("Buch", "book");    dict.addTranslation("Buch", "volume");    String trans = dict.getTranslation("Buch");    assertEquals("book, volume", trans); } 

The choice to expect a comma-separated list of translations and not a collection object can be considered as bad since it speculates on a future textual user interface which is not yet defined. Don't get too involved in that discussion, though. Test-driven development allows us to make the wrong decisions since we can revise them later when the design weakness becomes obvious. At that time we will have the tests handy to show that changing a single aspect left the rest of our application untouched.

And here again, the simplest solution is the best only until a new requirement, and thus new tests, require a more complex design:

 public class Dictionary {    public void addTranslation(String german, String translated) {       String before = this.getTranslation(german);       String now;       if (before == null) {          now = translated;       } else {          now = before + ", " + translated;       }       translations.put(german, now);    } } 

Let us review our test-driven development process as described so far in this chapter. At the beginning of each step, there was a test which was directly or indirectly motivated by the requirements specification. To be able to write this test, we had to make decisions about the public interface desired for our OUT (object under test). This public interface served both to "stimulate" the test object and to verify the correct behavior.

Once the compilation was successfully completed, we ran all tests created to this point, which resulted in a failure of the added or modified test, as expected. Next, we exercised some thought about the simplest way to realize the behavior specified in the test case. We used the "old" test cases as the constraint conditions; that is, the more test cases we had to satisfy, the more complex became our program design. At the end of each test-code cycle we reviewed the existing design, searching for refactoring opportunities to improve, which is to say, simplify it.

This approach drove and controlled the development of our production code from the tests. Note, however, that there is also an opposite control mechanism: as soon as we find that a method has not been fully programmed, despite a green bar, there is a strong hint that there are still tests missing.

So far, our tests have concentrated exclusively on the externally visible behavior of the OUT. In Java this theoretically includes all methods (and variables) with public, protected, or package scope visibility. In practice we restrict ourselves to use only what is intended to be used from the outside, that is from client code, which usually leaves out protected methods and members available for subclassing. This approach offers several benefits:

  • The tests document the intended use of the tested class. This kind of documentation is always consistent, in contrast to prosaic documentation.

  • Internal restructuring has no impact on the tests. For example, if we checked the structure of the hash map used, then a conversion to an independent class for each of the translations would be much more expensive.

This is why our guideline tells us to always use only the public interface in tests too. We will see later that we cannot always stick to this idealistic goal.

[1]Remember, a green bar is JUnit talk for having all tests run successfully.

[2]One reviewer suggested to look at this as a (programming) game where two players—the tester and the coder—try to outwit each other. I wonder if we programmers must have split personalities to develop successful code on our own?




Unit Testing in Java. How Tests Drive the Code
Unit Testing in Java: How Tests Drive the Code (The Morgan Kaufmann Series in Software Engineering and Programming)
ISBN: 1558608680
EAN: 2147483647
Year: 2003
Pages: 144
Authors: Johannes Link

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net