1.3 Classic Testing


1.3 Classic Testing

One particularity of the XP approach was briefly mentioned above: the test-first approach, a.k.a. test-driven development. "Test-first" means that the test is written before the actual implementation code. Before jumping into explaining what test-first means exactly (which we do in the next section), we use the following example of a small programming job to first look at the drawbacks of classic, subsequent testing.

We want to program a dictionary to translate a German text into a language of our choice, say English. This dictionary, in the form of a class, Dictionary, is initialized with a word file and allows us to query the translation of a German word. The program should allow several translation alternatives.

This description of requirements is sufficiently clear and compact for us to write the program code in one iteration. We will define missing details, like the exact format of the word file, while programming. The "classic" iteration includes (at least) the following steps: detailed design, implementation, and subsequent tests.

We use a UML class diagram as our initial design (Figure 1.1).


Figure 1.1: Class diagram of the dictionary.

Our Java implementation of the class Translation consists only of the constructor and two get methods:

 /**  * Represents a possible translation of a  * German word  */ public class Translation {    private String germanWord;    private String translation;    public Translation(String germanWord,                       String translation) {       this.germanWord = germanWord;       this.translation = translation;    }    public String getGermanWord() {       return germanWord;    }    public String getTranslation() {       return translation;    } } 

During their initialization, objects of the Dictionary class generate Translation objects and add them to an internal list. When querying the translation in the getTranslations() method, this list is iterated and the output string is built:

 import java.io.*; import java.util.List; import java.util.ArrayList; import java.util.Iterator; /**  * Dictionary for translation of German words into  * another language.  * The dictionary is initialized with a word file.  */ public class Dictionary {    private List entries = new ArrayList();    public Dictionary(String filename) throws IOException {       this.initializeFromReader(new BufferedReader(          new FileReader(filename))); }    /**     * Supplies a translation of the German word.     * If there are several alternatives, then these     * are appended, separated by a comma.     */    public String getTranslations(String germanWord) {       StringBuffer translations = new StringBuffer();       Iterator i = entries.iterator();       while (i.hasNext()) {          Translation each = (Translation) i.next();          if (each.getGermanWord().equals(germanWord)) {             if (translations.length() > 0) {                translations.append(", ");             }             translations.append(each.getTransaltion());          }       }       return translations.toString();    }    /**     * The word file to be read consists of     * 0 - n lines.     * Each line contains an entry in the following form:     * '<germanWord>=<translation>'     */    private final void initializeFromReader(       BufferedReader aReader) throws IOException {       String line = aReader.readLine();       while (line != null) {          int index = line.indexOf('=');          if (index != -1) {             String germanWord = line.substring(0, index);             String translation = line.substring(                index + 1, line.length());             Translation entry =                new Translation(germanWord, translation);             entries.add(entry);          }          line = aReader.readLine();       }    } } 

So far, all of this looks quite easy; only the tests are missing yet. For example, we could accommodate the tests in a separate class, DictionaryTester, with all single test cases in its main() method:

 public class DictionaryTester {    /**     * Start all test cases for the Dictionary class     */    public static void main(String[] args) {       testCase1();       testCase2();       /*...*/    } } 

The test cases are then programmed roughly in the following steps:

  1. Create a word file.

  2. Use this file to create an instance of the Dictionary class.

  3. Query specific translations and check the results.

The simplest case of a word file with one word would look like this:

 public static void testcase1() {    String filename = "C:\\temp\\dictionary.txt";    try {       PrintWriter writer = new PrintWriter(          new FileOutputStream(filename));       writer.println("Wort=word");       writer.close();       Dictionary dictionary = new Dictionary(filename);       String translation =          dictionary.getTranslations("Wort");       if (!translation.equals("word")) {          System.out.println("Test case 1 failed." +                             " Word found: " + translation);       } else {          System.out.println("Test case 1 successful.");       }    } catch (Exception ex) {       System.out.println("Test case 1 failed." +                          " Unexpected exception.");       System.out.println(ex.toString());    } } 

It would now be useful to have several test cases with a different number of entries in the file (0, 1, 2, and many), with identical entries, with several translations of the same word, to search for words that begin with uppercase or lowercase letters, and so on.

Unfortunately, this approach has a significant drawback: each single test case has to make a detour over a word file. This situation is not only complicated, it can also lead to nasty problems when creating and over-writing files. This problem would be avoided if we could add Translation objects to the Dictionary object without going a long way over a file. But is it really worth changing the implementation and giving up our hard earned encapsulation just for the test?

The same question comes up when we want to test the behavior while reading faulty word files. If an error occurs in the middle of a file, we would like to know how many translations have already been read. But there is no method available for this query yet. Also, we haven't thought about how the dictionary should generally react in case of errors. Should it ignore errors? Throw an exception? Output errors?

We would not have had to deal with a number of problems had we written the tests first and turned to the implementation after.

  • We would have had to think about error behavior and clarify any specification that may have been missing in advance.

  • Methods we need for test purposes would automatically have reached the (public or protected) interface, because a test is treated like any other "client" of our class.

  • A closer look would have lead us to the question of whether or not our Translation class is really necessary or whether it would have been sufficient to use a hash map.

Chapter 3 will further elaborate the above example by use of the test-first approach and arrive at a different design, which simplifies not only the tests but also the program itself.




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