Junit private methods: How to unit test private methods and classes

Last updated : 31 March 2021

Powermock is an open-source Java framework. Powermock leverages the capabilities of frameworks like Mockito to create mock objects in unit testing. Unit testing private methods and classes are possible by using Powermock's Whitebox. The Whitebox uses reflection to create inner class instances and invoke private methods outside the class file they are declared.

If you have been researching how to unit test private methods and classes, you may have come across various debates about best practices. I am confident that "Don't unit test private methods" is not the answer you want to find. If unit testing private methods and classes are inevitable for your situation, this article explains how to test private methods and classes.

What is Whitebox in JUnit?

org.powermock.reflect.Whitebox provides various utilities for accessing the internals of a class. These utilities include instantiating private classes and invoking private methods by using reflection. Whitebox is a utility intended for tests.

Unit test class with private methods and inner class

The below is the test candidate class with private methods and an inner class. Note that those methods and inner class are not accessible outside the class they are defined.

package com.learnbestcoding.junit;
import java.util.ArrayList;
import java.util.List;

public class Util 
{
    public void runMethods(String[] args)
    {
	//Public method uses all private methods
    }
    private static int getNumber()
    {
	return 1;
    }
    private List<String> getLanguages()
    {
    	List languages = new ArrayList<String>();
    	languages.add("Java");
    	languages.add("Sql");
    	languages.add("Javascript");
    	return languages;
    }
    private void addLanguage(List languages, String language)
    {
    	languages.add(language);
    }
    private ProgrammingLanguages getJavaLanguageClass()
    {
    	ProgrammingLanguages languages = new ProgrammingLanguages();
    	languages.setLanguageId(1);
    	languages.setLanguageName("Java");
    	languages.setNoOfEnrolments(100);
    	return languages;
    }
    private ProgrammingLanguages createLanguageClass(int languageId, String languageName, int noOfEnrolments)
    {
    	ProgrammingLanguages languages = new ProgrammingLanguages();
    	languages.setLanguageId(languageId);
    	languages.setLanguageName(languageName);
    	languages.setNoOfEnrolments(noOfEnrolments);
    	return languages;
    }
    private List<ProgrammingLanguages> addLanguageClass(ProgrammingLanguages language)
    {
    	List<ProgrammingLanguages> languageList = new ArrayList<ProgrammingLanguages>();
    	languageList.add(language);
    	return languageList;
    }
    class ProgrammingLanguages{
    	int languageId;
    	String languageName;
    	int noOfEnrolments;
    	
		public int getLanguageId() {
			return languageId;
		}
		public void setLanguageId(int languageId) {
			this.languageId = languageId;
		}
		public String getLanguageName() {
			return languageName;
		}
		public void setLanguageName(String languageName) {
			this.languageName = languageName;
		}
		public int getNoOfEnrolments() {
			return noOfEnrolments;
		}
		public void setNoOfEnrolments(int noOfEnrolments) {
			this.noOfEnrolments = noOfEnrolments;
		}
    }
}

Adding Junit and Powermock

Whitebox is part of powermock. Therefore, we need Junit and Powermock dependencies in the pom.xml file.
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.learnbestcoding</groupId>
	<artifactId>junit</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>
	<name>junit</name>
	<url>http://maven.apache.org</url>
	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	</properties>
	<dependencies>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.13.2</version>
		</dependency>
		<dependency>
			<groupId>org.powermock</groupId>
			<artifactId>powermock-api-mockito</artifactId>
			<version>1.7.4</version>
		</dependency>
		<dependency>
			<groupId>org.powermock</groupId>
			<artifactId>powermock-module-junit4</artifactId>
			<version>1.7.4</version>
		</dependency>
	</dependencies>
</project>

Junit Whitebox project structure

Figure 1 : Junit Whitebox project structure

Diffeerent test scenarios

In this tutorial, we will discuss six different scenarios to test private classes and methods. These scenarios include simple private methods with no arguments to private methods that accept private class arguments and return private classes.

Unit test private method with return value

The private method getNumber() is a simple method without any arguments and returns 1 for every execution. The important thing is not the method logic but how you invoke this method outside the method scope. The below code illustrates how to invoke a private method with no arguments and a simple return value. The getNumber is the private method name in Util.java class and Whitebox.invokeMethod(new Util(),"getNumber") returns the method return value.

package com.learnbestcoding.junit;
import static org.junit.Assert.assertEquals;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.reflect.Whitebox;

@RunWith(PowerMockRunner.class)
public class UtilTest {	
    @Test
    public void test_getNumber() throws Exception
    {
	int obj = Whitebox.invokeMethod(new Util(),"getNumber");
	assertEquals(obj, 1);
    }
}

Unit test private method that returns a list

The private method getLanguages() accepts no arguments and returns an ArrayList. The getLanguages is the private method name in Util.java class and Whitebox.invokeMethod(new Util(),"getLanguages") returns the method return value, which is an ArrayList.

@Test
public void test_getLanguages() throws Exception
{
  List obj = Whitebox.invokeMethod(new Util(),"getLanguages");
  assertEquals(obj.get(0), "Java");
}

Unit test private method that accepts arguments

The private method addLanguage() accepts two arguments and does not return a value. The addLanguage is the private method name in Util.java class, and method arguments are passed along with the method call Whitebox.invokeMethod(new Util(),"addLanguage",languages,"Python") The changes are mane to the variable reference languages.

@Test
public void test_addLanguage() throws Exception
{
  List languages = new ArrayList<String>();
  languages.add("Java");
  languages.add("Sql");
  languages.add("Javascript");
  Whitebox.invokeMethod(new Util(),"addLanguage",languages,"Python");
  assertEquals(languages.get(3), "Python");
}

Unit test private method that returns an inner class

The private method getJavaLanguageClass() accepts no arguments and returns an inner class. Note that the test class has no access to the private method and inner class. The getJavaLanguageClass is the private method name in Util.java class, and ProgrammingLanguages is the inner class the method returns. Whitebox.invokeMethod(new Util(),"getJavaLanguageClass") returns the innerclass and we can access its methods with obj.getClass().getDeclaredField("languageId").get(obj). The languageId is the property name.

@Test
public void test_getLanguageClass() throws Exception
{
  Object obj =Whitebox.invokeMethod(new Util(),"getJavaLanguageClass");
  assertEquals(obj.getClass().getDeclaredField("languageId").get(obj), 1);
  assertEquals(obj.getClass().getDeclaredField("languageName").get(obj), "Java");
  assertEquals(obj.getClass().getDeclaredField("noOfEnrolments").get(obj), 100);
}

Unit test private method with arguments and returns an inner class

The private method createLanguageClass() accepts arguments and returns a private class. The createLanguageClass is the private method name in Util.java class, and ProgrammingLanguages is the inner class the method returns. Whitebox.invokeMethod(new Util(),"createLanguageClass",2,"Sql",240) passes the arguments to the private class and returns the return value of the private method. The return value is also a type of private class and the properties are accessible with obj.getClass().getDeclaredField("languageId").get(obj). The languageId is the class property name.

@Test
public void test_createLanguageClass() throws Exception
{
  Object obj =Whitebox.invokeMethod(new Util(),"createLanguageClass",2,"Sql",240);
  assertEquals(obj.getClass().getDeclaredField("languageId").get(obj), 2);
  assertEquals(obj.getClass().getDeclaredField("languageName").get(obj), "Sql");
  assertEquals(obj.getClass().getDeclaredField("noOfEnrolments").get(obj), 240);
}

Unit test private method that accepts private class and returns private class

The private method addLanguageClass() accepts private class as arguments and returns a private class. The addLanguageClass is the private method name in Util.java class, and ProgrammingLanguages is the inner class the method returns. Here is how to implement it.

@Test
public void test_addLanguageClass() throws Exception
{
  Object programmingLanguages = new Object();
  Class clazz = Whitebox.getInnerClassType(Util.class, "ProgrammingLanguages");
  Constructor constructor = Whitebox.getConstructor(clazz,Util.class);
  programmingLanguages = constructor.newInstance(new Util());
  Field field =  null;

  field = programmingLanguages.getClass().getDeclaredField("languageId");
  field.setAccessible(true);
  field.set(programmingLanguages, 1);

  field = programmingLanguages.getClass().getDeclaredField("languageName");
  field.setAccessible(true);
  field.set(programmingLanguages, "Java");

  field = programmingLanguages.getClass().getDeclaredField("noOfEnrolments");
  field.setAccessible(true);
  field.set(programmingLanguages, 100);

  List obj = Whitebox.invokeMethod(new Util(),"addLanguageClass",programmingLanguages);
  assertEquals(obj.get(0).getClass().getDeclaredField("languageId").get(obj.get(0)), 1);
  assertEquals(obj.get(0).getClass().getDeclaredField("languageName").get(obj.get(0)), "Java");
  assertEquals(obj.get(0).getClass().getDeclaredField("noOfEnrolments").get(obj.get(0)), 100);
}

Full Junit test class to test private methods and classes

package com.learnbestcoding.junit;
import static org.junit.Assert.assertEquals;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.reflect.Whitebox;

@RunWith(PowerMockRunner.class)
public class UtilTest {
@Test
public void test_getNumber() throws Exception
{
  int obj = Whitebox.invokeMethod(new Util(),"getNumber");
  assertEquals(obj, 1);
}
@Test
public void test_getLanguages() throws Exception
{
  List obj = Whitebox.invokeMethod(new Util(),"getLanguages");
  assertEquals(obj.get(0), "Java");
}
@Test
public void test_addLanguage() throws Exception
{
  List languages = new ArrayList<String>();
  languages.add("Java");
  languages.add("Sql");
  languages.add("Javascript");
  Whitebox.invokeMethod(new Util(),"addLanguage",languages,"Python");
  assertEquals(languages.get(3), "Python");
}
@Test
public void test_getLanguageClass() throws Exception
{
  Object obj =Whitebox.invokeMethod(new Util(),"getJavaLanguageClass");
  assertEquals(obj.getClass().getDeclaredField("languageId").get(obj), 1);
  assertEquals(obj.getClass().getDeclaredField("languageName").get(obj), "Java");
  assertEquals(obj.getClass().getDeclaredField("noOfEnrolments").get(obj), 100);
}
@Test
public void test_createLanguageClass() throws Exception
{
  Object obj =Whitebox.invokeMethod(new Util(),"createLanguageClass",2,"Sql",240);
  assertEquals(obj.getClass().getDeclaredField("languageId").get(obj), 2);
  assertEquals(obj.getClass().getDeclaredField("languageName").get(obj), "Sql");
  assertEquals(obj.getClass().getDeclaredField("noOfEnrolments").get(obj), 240);
}
  @Test
  public void test_addLanguageClass() throws Exception
  {
    Object programmingLanguages = new Object();
    Class clazz = Whitebox.getInnerClassType(Util.class, "ProgrammingLanguages");
    Constructor constructor = Whitebox.getConstructor(clazz,Util.class);
    programmingLanguages = constructor.newInstance(new Util());
    Field field =  null;

    field = programmingLanguages.getClass().getDeclaredField("languageId");
    field.setAccessible(true);
    field.set(programmingLanguages, 1);

    field = programmingLanguages.getClass().getDeclaredField("languageName");
    field.setAccessible(true);
    field.set(programmingLanguages, "Java");

    field = programmingLanguages.getClass().getDeclaredField("noOfEnrolments");
    field.setAccessible(true);
    field.set(programmingLanguages, 100);

    List obj = Whitebox.invokeMethod(new Util(),"addLanguageClass",programmingLanguages);
    assertEquals(obj.get(0).getClass().getDeclaredField("languageId").get(obj.get(0)), 1);
    assertEquals(obj.get(0).getClass().getDeclaredField("languageName").get(obj.get(0)), "Java");
    assertEquals(obj.get(0).getClass().getDeclaredField("noOfEnrolments").get(obj.get(0)), 100);
  }
}

Conclusion

Powermock offers Whitebox utility offers utilities to unit test private methods and private classes using reflection. Whitebox makes private methods and classes accessible outside their defined scope and visibility.

Lance
By: Lance
Lance is a software engineer with over 15 years of experience in full-stack software development.
Read more...

Comments are disabled

No Comments