[JAVA] Refactoring in JUnit

Significance of refactoring in JUnit

For refactoring in JUnit, improving testability is a top priority. Classes with complicated states to hold, methods with many arguments, methods with wide responsibilities, etc. It is a complicated test target that is difficult to verify. Besides, the implementation is not completed yet, the service is stopped, and the test target depends on it. There are also situations where unit tests cannot be executed, such as when an external Web service cannot be used.

Refactoring in JUnit provides testability for these test cases To improve, change only the ** internal structure ** without changing the ** behavior ** under test.

What is testability?

Testability is the ease of testing. Generally, execution timing, execution environment, execution order, etc. Test cases that are affected by the external environment are said to have low testability.

What is refactoring?

As mentioned above, refactoring means changing only the ** internal structure ** without changing the ** behavior ** of the test target. This is a technique for organizing source code.

Supplement: Precautions for refactoring When refactoring It is desirable that a unit test of the target class / method exists. That's because refactoring shouldn't change the behavior. Therefore, by changing the internal structure while checking the behavior by unit test Developers can rest assured that they can improve their production code.

Since this is just an introduction to simple refactoring techniques, See specialized books for more detailed refactoring techniques.

Refactoring techniques in JUnit

Overview

Here, the degree of module coupling is weakened by extracting the process, Replace stubs and mock that return reliable results instead of uncertainties I will introduce the method.

Below, the design becomes weaker as it goes down. Regarding the degree of coupling, the article "About module coupling" was helpful.

  1. Extract as a method
  2. Extract as class
  3. Extract as interface

The optimal pattern does not necessarily mean that the degree of coupling is weak. There is also the danger of over-engineering in refactoring. The implemented implementation is too large for what you want to achieve. When actually refactoring, what you want to achieve and the implementation cost Determine if it is balanced and perform appropriate refactoring.

The following is an example of a test case that confirms that the system time is set. We will introduce various refactoring methods.

Test code before refactoring

Production code


public class Dateutils {
    // (1)Class initialization(=Instantiation)Set at the time
    Date date = new Date();
    
    public void doSomethingToDate() {
        // (2)Set at method execution
        This.date = new date();
        //Some processing
    }
} 

Test code


public class DateUtilsTest {
    @Test
public void doSomethingToDate sets date to current time() throws Exception {
        // (1)Class instantiation sets date to system time
        DateUtils sut = new DateUtils();
        // (2)The process in the method overwrites the system time on date
        sut.doSomethingToDate();
        // (3)The system time passed to the is method is the system time at this point
        assertThat(sut.date, is(new Date()));
    }
} 

What's detrimental to testability in this implementation is The result of the execution can be either successful or unsuccessful.

The system time assumed by the assertThat method is the system time set at the time of (3), The `` `sut.date``` passed as the measured value is the value set at the time of executing the method (2). Since the java.util.Date class holds the date and time up to milliseconds, the execution time from (2) to (3) If it takes more than a millisecond, the test will fail. (It rarely occurs in the above implementation, but it fails if processing is stopped for 1 second in doSomethingToDate)

Extract as a method

In the following implementation, ** Get system time ** is cut out as a method. It is replaced with a stub by overriding in the test code. In this way, the test case that depended on the uncertain factor of ** method execution timing ** It has been refactored and changed so that a reliable value can be set from the test code side.

Strictly speaking, getting the system time is also a method under test. From the point of view of rewriting the value from the test code side, These are not perfect tests. However, leaving uncertainties behind, ** Tested Class ** Rather, the problem is that the test results of the larger framework are not stable. From a testability point of view, processing that is difficult to handle during testing It is good to design it so that it can be extracted as an individual method and easily operated from the test code.

Production code


public class ExtractMethod {
    Date date = new Date();
    
    public void doSomethingToDate() {
        // (1)By(2)The system time at the time of test execution is set instead of the system date at the time of method execution
        this.date = getNowDate();
        //Some processing
    }

    Date getNowDate() {
        // (2)The original code sets the system time when the method is executed
        return new Date();
    }
}

Test code


public class ExtractMethodTest {
    @Test
public void doSomethingToDate sets date to current time() throws Exception {
        final Date current = new Date();
        ExtractMethod sut = new ExtractMethod() {
            // (1)Changed to overwrite the process of setNowDate and set the instance of current
            @Override
            Date getNowDate() {
                return current;
            }
        };
        sut.doSomethingToDate();
        assertThat(sut.date, is(sameInstance(current)));
    }
}

Extract as a class

As for making, the target of extraction and acquisition is just a class from a method. By extracting as a class, it can be used from multiple test target classes. However, from the viewpoint of test independence, do not over-commonize. (Since the amount of coding has increased for what I want to do, it seems to be over-designed)

Production code(Extracted class)


public class DateFactory {
    Date getNowDate() {
        return new Date();
    }
}

Production code(Tested class)


public class ExtractClass {
    DateFactory dateFactory = new DateFactory();
    Date date = new Date();
    
    public void doSomethingToDate() {
        this.date = dateFactory.getNowDate();
        //Some processing
    }
}

Test code


public class ExtractClassTest {
    @Test
public void doSomethingToDate sets date to current time() {
        final Date current = new Date();
        ExtractClass sut = new ExtractClass();
        sut.dateFactory = new DateFactory() {
            @Override
            Date getNowDate() {
                return current;
            }
        };
        sut.doSomethingToDate();
        assertThat(sut.date, is(sameInstance(current)));
    }
}

Extract as interface

By extracting as an interface, not only can it be used in common by multiple classes, but also The processing performed in the getNowDate method can now be changed flexibly. (Completely over-engineered in the implementation below)

Production code(Extracted interface)


public interface DateFactory {
    Date getNowDate();
}

Production code(Interface implementation class)


public class DateFactoryImpl implements DateFactory {
    @Override
    public Date getNowDate() {
        return new Date();
    }
}

Production code(Tested class)


public class ExtractClass {
    DateFactory dateFactory = new DateFactoryImpl();
    Date date = new Date();
    
    public void doSomethingToDate() {
        this.date = dateFactory.getNowDate();
        //Some processing
    }
}

Test code


public class ExtractClassTest {
    @Test
public void doSomethingToDate sets date to current time() {
        final Date current = new Date();
        ExtractClass sut = new ExtractClass();
        sut.dateFactory = new DateFactory() {
            @Override
            Date getNowDate() {
                return current;
            }
        };
        sut.doSomethingToDate();
        assertThat(sut.date, is(sameInstance(current)));
    }
}

References

This article was written with reference to the following information.

-[Introduction to JUnit Practice ── Systematic Unit Testing Techniques](https://www.amazon.co.jp/JUnit%E5%AE%9F%E8%B7%B5%E5%85%A5%E9 % 96% 80-% E4% BD% 93% E7% B3% BB% E7% 9A% 84% E3% 81% AB% E5% AD% A6% E3% 81% B6% E3% 83% A6% E3% 83% 8B% E3% 83% 83% E3% 83% 88% E3% 83% 86% E3% 82% B9% E3% 83% 88% E3% 81% AE% E6% 8A% 80% E6% B3% 95-WEB-PRESS-plus-ebook / dp / B07JL78S95)

Recommended Posts

Refactoring in JUnit
About validation methods in JUnit
Refactoring: Make Blackjack in Java
Test private methods in JUnit
Test private methods in JUnit
How to run JUnit in Eclipse
Output JUnit test report in Maven
What are the rules in JUnit?
JUnit 4 notes
How to filter JUnit Test in Gradle
Control test order in Junit4 with enumeration
Refactoring Ruby
Enjoy JUnit 5 in Eclipse before official release
Getting Started with Parameterization Testing in JUnit
[Personal] JUnit5 memorandum memo (work in progress)
How to fix system date in JUnit
JUnit memorandum
Refactoring Ruby
Refactoring Ruby
[Ruby] Basic key to be strong in refactoring
Run modular project tests in Gradle (JUnit5 + TestFX)
[For beginners] I tried using JUnit 5 in Eclipse
JUnit 5: How to write test cases in enum
[Java beginner's anguish] Hard-to-test code implemented in Junit