Previously in What you should be aware of when writing Java, ** private methods can be called by reflection, so access modifiers are used only for testing. I wrote that you shouldn't delete it **. So, here is an example.
If it is a field instead of a method, write it like this. → Refer / set private variables by reflection
2018/12/26 Added "Call private constructor"
We are aware that there is an opinion that "it should be called from the public method to test the private method consistently". However, depending on the project, there are many cases that include a method-based test = private method. It's a rule. Everyone writes the test code accordingly. Even if you break it, you will only be told to rewrite it. In some cases, there is only one public method in the class in the business logic. In such a case, the test is also difficult. It is easier to write a test for a public method after guaranteeing the processing for each private method with a test. That's why the pros and cons of testing per private method are out of scope.
The sample code omits Javadoc and some kettles. I have confirmed the operation so that it works with copy and paste, but it is just a sample code and reference.
Sample.java
public class Sample {
private String strValue = null;
public Sample(String value) {
this.strValue = (value == null) ? "" : value;
}
/** 5.Call private constructor*/
private Sample() {
}
/** 1.Calling a non-static method*/
private boolean equals(String value) {
return this.strValue.equals(value);
}
/** 2.Call static method*/
private static boolean isEmpty(String value) {
return (value == null || "".equals(value)) ? true : false;
}
/** 3.Call with no return value / argument*/
private static void dispMessage() {
System.out.println("Hello world!");
}
/** 4.Check for exceptions*/
private void setValue(String value) {
if (value == null || value.isEmpty()) {
throw new IllegalArgumentException("argument is empty.");
}
this.strValue = value;
}
}
doPrivateMethod is the method for calling the private method under test. In types, the argument types of the private method are stored in an array in order from the first argument and passed. args passes the arguments themselves as well. Since the method name to be tested, the type and number of arguments are specified, the test will fail if that side is changed by refactoring or the like. (InvocationTargetException occurs)
SampleTest.java
import static org.junit.Assert.*;
import java.lang.reflect.Method;
import org.junit.Test;
public class SampleTest {
@Test
public void test_equals() throws Exception {
Sample testee = new Sample("test");
assertTrue((boolean) this.doPrivateMethod(
testee, "equals", new Class[]{String.class}, new Object[]{"test"}));
}
/**
*Non-static method call.
*
* @param obj Tested object
* @param name Tested method name
* @param types Argument type of the method under test
* @param args Arguments of the method under test
* @return Return value of non-static method
*/
private Object doPrivateMethod(
Object obj, String name, Class[] types, Object[] args) throws Exception {
//Get information about the method under test
Method method = obj.getClass().getDeclaredMethod(name, types);
//Remove access restrictions to the method under test
method.setAccessible(true);
//Tested method call
return method.invoke(obj, args);
}
}
doStaticPrivateMethod is the method for calling the private method under test. The ones that are vulnerable to refactoring are the same as 1. Since it is a call to a static method, no object is passed as an argument.
SampleTest.java
import static org.junit.Assert.*;
import java.lang.reflect.Method;
import org.junit.Test;
public class SampleTest {
@Test
public void test_isEmpty() throws Exception {
assertTrue((boolean) this.doStaticPrivateMethod(
"isEmpty", new Class[]{String.class}, new Object[]{null}));
}
/**
*static method call.
*
* @param name Tested method name
* @param types Argument type of the method under test
* @param args Arguments of the method under test
* @return static method return value
*/
private Object doStaticPrivateMethod(
String name, Class[] types, Object[] args) throws Exception {
//Get information about the method under test
Method method = Sample.class.getDeclaredMethod(name, types);
//Remove access restrictions to the method under test
method.setAccessible(true);
//Tested method call
return method.invoke(null, args);
}
}
assertTrue((boolean) this.doStaticPrivateMethod("isEmpty", new Class[]{String.class}, new Object[]{null}));
If you want to pass null in a method with arguments, put it in an array. An array is required even with one argument. If you pass it as it is, it will fail.
If the method under test is void, there is no return value. (Null even if you try to pick it up) Pass null when calling a method with no arguments. Unlike 2., it doesn't have to be an array.
SampleTest.java
import static org.junit.Assert.*;
import java.lang.reflect.Method;
import org.junit.Test;
public class SampleTest {
@Test
public void test_dispMessage() throws Exception {
this.doStaticPrivateMethod("dispMessage", null, null);
}
}
** Added from comments **
this.doStaticPrivateMethod("dispMessage", null, null);
It is not asserted because it is a sample calling method. Even in the void method test, confirmation of the processing result is indispensable. For example, if you process a DB or file, assert the result. In the case of Sample class, check whether the character string output to the console is correct. For example, like this.
//Standard output result redirection
ByteArrayOutputStream out = new ByteArrayOutputStream();
System.setOut(new PrintStream(out));
//Tested call
this.doStaticPrivateMethod("dispMessage", null, null);
//Assert
assertEquals("Hello world!" + System.lineSeparator(), out.toString());
If you want to run the sample, import ByteArrayOutputStream and PrintStream.
All invocation runtime errors are wrapped and thrown with ** InvocationTargetException **. Therefore, the check for exceptions thrown in the test target is performed by extracting the contents wrapped by InvocationTargetException. Also, in the case of a test that confirms that an exception is thrown, if the exception is not thrown, or if an exception other than the intended exception occurs, the test will fail on JUnit, so ** fail (explicitly ** fail ( );**call.
SampleTest.java
import static org.junit.Assert.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.junit.Test;
public class SampleTest {
@Test
public void test_setValue() throws Exception {
try {
Sample testee = new Sample("test");
this.doPrivateMethod(
testee, "setValue", new Class[]{String.class}, new Object[]{""});
//Test failed
fail();
} catch (InvocationTargetException e) {
//Exception type checking
assertTrue(e.getCause() instanceof IllegalArgumentException);
//Exception message check
assertEquals("argument is empty.", e.getCause().getMessage());
} catch (Exception e) {
//Test failed
fail(e.getMessage());
}
}
}
Isn't it writing a private constructor with no arguments to prevent it from being called from the outside in a static class? That is the test method. It is used in projects where "100% coverage" is a condition. The comment says "Write the class name with a fully qualified name", but since the sample class this time is not packaged, only the class name is described. Normally, I think there is a package, so it will not work unless it is a fully qualified name.
SampleTest.java
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.junit.Assert.*;
import java.lang.reflect.Constructor;
import org.junit.Test;
public class SampleTest {
@Test
public void test_private_constructor() {
try {
//Describe the class name as a fully qualified name
Class<?> clazz = Class.forName("Sample");
Constructor<?>[] constructors = clazz.getDeclaredConstructors();
constructors[1].setAccessible(true);
Object obj = constructors[1].newInstance();
assertNotNull(obj);
assertThat(obj, instanceOf(clazz));
} catch (Exception e) {
fail(e.getMessage());
}
}
}
constructors[1].setAccessible(true);
Here, describe the order described in the Sample class with 0 origin. In this case, the private constructor you want to test is "1" because it is described in the second of the constructors of the Sample class. If you only have a constructor to test, "0" is fine.
Have a good Java life! If something happens, I will write it in a postscript or another article.
Recommended Posts