[JAVA] Implement test doubles in JUnit without using external libraries

What is a test double?

A word derived from the English word "double", which means a substitute for a performer or a shadow warrior. Generally called mock or stub.

Why use test double

In the unit test of the system, the test target is There are few cases where it does not depend on other classes or external elements. A test that has a dependency means that it has tested not only the target but also the dependency.

The advantage is that the test is performed in the same state as when the system is running. There is an aspect that the value of the content guaranteed by the test results increases. On the other hand, the disadvantage is that the scope of cause analysis becomes wider when the test fails. If the processing result of the dependent object cannot be predicted, the accuracy of verification will drop.

As a countermeasure against the above disadvantages, instead of the factors that affect the test There is a way to increase the independence of the test by using a substitute that is easy to test. It is the use of stubs and mock (test double) to achieve these.

Difference between stub and mock

Both are used as substitutes for the classes and modules that the test target depends on. There is no difference in terms of classes and modules. Therefore, it is often used as a synonym, but in a narrow sense, there are the following differences.

stub

** The purpose is to make dependent objects behave in a predictable way **.

In addition, it is used as a substitute for the following objects.

--Behavior unpredictable --Implementation is not completed --High execution cost and not easy to use --Strongly dependent on the execution environment

mock

** The purpose is to check if the dependent object was called during execution **.

Various test doubles

A stub that returns a fixed value

For processes that are difficult to obtain the desired value, such as random numbers and system dates, Extract it as a method and class and switch it with a stub to make it easier to test. There are various ways to implement stubs, but for details, see "[Refactoring method in JUnit](https://qiita.com/maple_syrup/items/c370f3f850972aafd101#junit%E3%81%AB%E3%81%8A%E3". % 81% 91% E3% 82% 8B% E3% 83% AA% E3% 83% 95% E3% 82% A1% E3% 82% AF% E3% 82% BF% E3% 83% AA% E3% 83 % B3% E3% 82% B0% E3% 81% AE% E6% 89% 8B% E6% B3% 95) ".

In the following example, the process of acquiring random numbers is cut out as a method. You have created and overridden a stub method in your test code.

Production code


public class GetRandomElementFromList {
	public <T> T choice(List<T> elements) {
		if (elements.size() == 0) {
			return null;
		}
		int idx = nextInt() % elements.size();
		return elements.get(idx);
	}

	int nextInt() {
		return new Random().nextInt();
	}
}

Test code


public class GetRandomElementFromListTest {
	private List<String> elements = new ArrayList<>();
	
	@Before
	public void setUp() {
		elements.add("A");
		elements.add("B");
	}

	@Test
returns A with public void choice() {
		GetRandomElementFromList sut = new GetRandomElementFromList() {
			@Override
			int nextInt() {
				return 0;
			}
		};
		assertThat(sut.choice(elements), is("A"));
	}
	
	@Test
Returns B with public void choice() {
		GetRandomElementFromList sut = new GetRandomElementFromList() {
			@Override
			int nextInt() {
				return 1;
			}
		};
		assertThat(sut.choice(elements), is("B"));
	}
}

Stub that throws an exception

There are many cases where it is difficult to raise an exception, such as a database connection error. Even in such a case, a method that throws an exception By cutting it into small pieces, it is possible to switch by stub.

Spy: A mock that monitors object calls

The basic unit test verifies the input value and output value like the assertThat method. Therefore, it is easy to test an object whose test target returns a return value and does not retain its state. On the other hand, the test case when the state of the method without return value or the target object changes Verification becomes difficult. Verification becomes even more difficult when the state of dependent objects changes.

A typical example of side effects on dependent objects is There is verification of the contents written to the standard output and logger. (Because the state of dependent objects (standard output, etc.) changes depending on the execution of the test target) In such a case, write a logger or an object that performs standard output. It replaces with an object called a spy and monitors hidden processing. The spy wraps and implements the original object.

The following is an implementation example of test code for verifying the method that outputs logs.

Production code


public class OutputLog {

    Logger logger = Logger.getLogger(OutputLog.class.getName());
    
    public void outputLog() {
        logger.info("doSomething");
    }
}

Test code(Spy class)


public class SpyLogger extends Logger{

    final Logger base;
    //Class variable for referencing the log from the test side separately from the log output
    final StringBuffer log = new StringBuffer();
    
    public SpyLogger(Logger base) {
        super(base.getName(), base.getResourceBundleName());
        this.base = base;
    }
    
    //Method to be a stub
    @Override
    public void info(String message) {
        base.info(message);
        //Additional processing → Output to the StringBuffer that the spy has independently
        log.append(message);
    }
}

Test code(test case)


public class SpyExampleTest {
    @Test
    public void test() {
        OutputLog sut = new OutputLog();
        SpyLogger spyLogger = new SpyLogger(sut.logger);
        //Replace the logger under test with a spy logger
        sut.logger = spyLogger;
        sut.outputLog();
        assertThat(spyLogger.log.toString(), is("doSomething"));
    }
}

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

Implement test doubles in JUnit without using external libraries
Implement share button in Rails 6 without using Gem
Test field-injected class in Spring boot test without using Spring container
Test private methods in JUnit
Test private methods in JUnit
Implement Table Driven Test in Java 14
Output JUnit test report in Maven
How to implement UI automated test using image comparison in Selenium
[Spring MVC] Implement dynamic parameters included in URL without using Optional (~ Java7)
Control test order in Junit4 with enumeration
Map without using an array in java
Implement button transitions using link_to in Rails
[For beginners] I tried using JUnit 5 in Eclipse
Implement user edit / update function without using devise
JUnit 5: How to write test cases in enum
Implement partial match search function without using Ransuck
Implement star rating function using Raty in Rails6
Test code using mock with JUnit (EasyMock center)