4.9 Refactoring Code and Tests


4.9 Refactoring Code and Tests

Let's recapitulate the definition of refactoring:

Refactoring is the process of changing a software system in such a way that it does not alter the external behavior of the code yet improves its internal structure.[11]

At first sight, this definition suggests that nothing will change in the tests either in the course of refactoring. This holds true for all those tests that view the behavior of the component under refactoring (CUR) from the outside, for example, all acceptance tests. In contrast, unit tests are normally a mixture of specification-based and implementation-dependent tests. For this reason, changes in the course of refactoring cannot be avoided. We can identify the following basic types and their impact on unit tests:

  • Renaming methods, classes, or packages forces the renaming of test cases and move test classes.

  • Removing parameters results in the corresponding adaptation of the test cases. Sometimes test cases can become superfluous, such as when they differ from another test case only by a parameter that was removed.

  • Adding parameters requires not only a correction of the corresponding method calls, but often also expansion of the test fixture by the new parameter object.

  • Extracting a class causes the tests to be moved—when these tests concentrated on the behavior—which is now implemented by the new class. In addition, interaction tests between the original and the added class are necessary. After the extraction, it is also important to look at the new class as an independent CUT to identify missing test cases.

    A special case of extracting a class is the extraction of a subclass. This often leads to a parallel hierarchy of test classes (see Chapter 7, Section 7.1).

  • As a countermove, the inlining of a class means that tests have to be moved in opposite direction. This can cause some tests to lose their right to exist, especially the tests dealing with the interaction of both classes.

  • Moving a public method into another class has consequences similar to the extraction of a class, that is, either moving the test cases that concentrate on this method or duplicating the test cases, if the method remains in the old class and delegates the call to the new class.

  • Modifying the implementation of a single method has no impact on black-box tests, but implementation-dependent test cases have to be reconsidered. For example, if we were to modify the implementation of the sort() method from Section 4.4 as follows:

        private final static int MIN_QUICKSORT = 10;    public List sort(List unsorted) {       if (unsorted.size() < MIN_QUICKSORT) {          return bubbleSort(unsorted);       } else {          return quickSort(unsorted);       }    } 

    Then the correct test cases would no longer be equipped with 14 and 15, but with 9 and 10 elements.[12]

A common approach in refactoring is to first identify the test cases which should still be valid after refactoring. Next, you restructure in small steps and evaluate after each step whether some test is now superfluous or should be moved or expanded. The next action would always be the start of the remaining tests to validate your assumptions. If you think a refactoring task is complete, you once more swap your developer hat for your tester cap and reconsider all existing test cases with regard to adequacy and redundancy.

Marick [00] emphasizes not trying to desperately keep all existing tests when doing system modifications. The more complex the tested scenario, the more difficult it will be to adapt it subsequently to the new situation. Such complicated tests should sometimes be thrown away and replaced by new ones for reasons of cost.

The cost arising to adapt our test suites to the constantly changing program can be considerable, but it is seldom a waste of effort. One positive effect is that the developers are constantly confronted with their previous assumptions and get opportunities to improve things. After all, learning is a never ending process.

On the other hand, if we find that maintaining our test suites takes much more time than the actual modifications to the system, then this is a sign that we are writing the wrong tests. Here "wrong" means that we may be testing things that change too often, so that automated tests, such as testing private methods and attributes of a class, are of no use.

[11][Fowler99, p. xvi].

[12]A few people suggested making the constant MIN_QUICKSORT public and using it to build the appropriate test data in the test case. This would indeed save us the labor of changing the test when changing the value, but it would (a) reveal an implementation detail and (b) require more complex test logic.




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