Build with Confidence: A Guide to JUnit Tests
In an age of continuous delivery, Java developers have to be confident that their changes don’t break existing code, hence automated testing. There’s more than one valid approach to it, but how can you keep them straight?
In an age of continuous delivery, Java developers have to be confident that their changes don’t break existing code, hence automated testing. There’s more than one valid approach to it, but how can you keep them straight?
Nikhil (BTech) has automated finance sector code across everything from J2EE to Elasticsearch to Kafka. And, of course, all things AWS.
Expertise
PREVIOUSLY AT
In an age of continuous delivery, Java developers have to be confident that their changes don’t break existing code, hence automated testing. There’s more than one valid approach to it, but how can you keep them straight?
With the advancement of technology and industry moving from the waterfall model to Agile and now to DevOps, changes and enhancements in an application are deployed to production the minute they’re made. With code getting deployed to production this fast, we need to be confident that our changes work, and also that they don’t break any preexisting functionality.
To build this confidence, we must have a framework for automatic regression testing. For doing regression testing, there are many tests that should be carried out from an API-level point of view, but here we’ll cover two major types of tests:
- Unit testing, where any given test covers the smallest unit of a program (a function or procedure). It may or may not take some input parameters and may or may not return some values.
- Integration testing, where individual units are tested together to check whether all the units interact with each other as expected.
There are numerous frameworks available for every programming language. We will focus on writing unit and integration testing for a web app written in Java’s Spring framework.
Most of the time, we write methods in a class, and these, in turn, interact with methods of some other class. In today’s world—especially in enterprise applications—the complexity of applications is such that a single method might call more than one method of multiple classes. So when writing the unit test for such a method, we need a way to return mocked data from those calls. This is because the intent of this unit test is to test only one method and not all the calls that this particular method makes.
Let’s jump into Java unit testing in Spring using the JUnit framework. We’ll start with something you may have heard of: mocking.
What is Mocking and When Does It Come into the Picture?
Suppose you have a class, CalculateArea
, which has a function calculateArea(Type type, Double... args)
which calculates the area of a shape of the given type (circle, square, or rectangle.)
The code goes something like this in a normal application which does not use dependency injection:
public class CalculateArea {
SquareService squareService;
RectangleService rectangleService;
CircleService circleService;
CalculateArea(SquareService squareService, RectangleService rectangeService, CircleService circleService)
{
this.squareService = squareService;
this.rectangleService = rectangeService;
this.circleService = circleService;
}
public Double calculateArea(Type type, Double... r )
{
switch (type)
{
case RECTANGLE:
if(r.length >=2)
return rectangleService.area(r[0],r[1]);
else
throw new RuntimeException("Missing required params");
case SQUARE:
if(r.length >=1)
return squareService.area(r[0]);
else
throw new RuntimeException("Missing required param");
case CIRCLE:
if(r.length >=1)
return circleService.area(r[0]);
else
throw new RuntimeException("Missing required param");
default:
throw new RuntimeException("Operation not supported");
}
}
}
public class SquareService {
public Double area(double r)
{
return r * r;
}
}
public class RectangleService {
public Double area(Double r, Double h)
{
return r * h;
}
}
public class CircleService {
public Double area(Double r)
{
return Math.PI * r * r;
}
}
public enum Type {
RECTANGLE,SQUARE,CIRCLE;
}
Now, if we want to unit-test the function calculateArea()
of the class CalculateArea
, then our motive should be to check whether the switch
cases and exception conditions work. We should not test whether the shape services are returning the correct values, because as mentioned earlier, the motive of unit-testing a function is to test the logic of the function, not the logic of the calls the function is making.
So we will mock the values returned by individual service functions (e.g. rectangleService.area()
and test the calling function (e.g. CalculateArea.calculateArea()
) based on those mocked values.
A simple test case for the rectangle service—testing that calculateArea()
indeed calls rectangleService.area()
with the correct parameters—would look like this:
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
public class CalculateAreaTest {
RectangleService rectangleService;
SquareService squareService;
CircleService circleService;
CalculateArea calculateArea;
@Before
public void init()
{
rectangleService = Mockito.mock(RectangleService.class);
squareService = Mockito.mock(SquareService.class);
circleService = Mockito.mock(CircleService.class);
calculateArea = new CalculateArea(squareService,rectangleService,circleService);
}
@Test
public void calculateRectangleAreaTest()
{
Mockito.when(rectangleService.area(5.0d,4.0d)).thenReturn(20d);
Double calculatedArea = this.calculateArea.calculateArea(Type.RECTANGLE, 5.0d, 4.0d);
Assert.assertEquals(new Double(20d),calculatedArea);
}
}
Two main lines to note here are:
-
rectangleService = Mockito.mock(RectangleService.class);
—This creates a mock, which is not an actual object, but a mocked one. -
Mockito.when(rectangleService.area(5.0d,4.0d)).thenReturn(20d);
—This says that, when mocked, and therectangleService
object’sarea
method is called with the specified parameters, then return20d
.
Now, what happens when the above code is part of a Spring application?
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class CalculateArea {
SquareService squareService;
RectangleService rectangleService;
CircleService circleService;
public CalculateArea(@Autowired SquareService squareService, @Autowired RectangleService rectangeService, @Autowired CircleService circleService)
{
this.squareService = squareService;
this.rectangleService = rectangeService;
this.circleService = circleService;
}
public Double calculateArea(Type type, Double... r )
{
// (same implementation as before)
}
}
Here we have two annotations for the underlying Spring framework to detect at the time of context initialization:
-
@Component
: Creates a bean of typeCalculateArea
-
@Autowired
: Searches for the beansrectangleService
,squareService
, andcircleService
and injects them into the beancalculatedArea
Similarly, we create beans for other classes as well:
import org.springframework.stereotype.Service;
@Service
public class SquareService {
public Double area(double r)
{
return r*r;
}
}
import org.springframework.stereotype.Service;
@Service
public class CircleService {
public Double area(Double r)
{
return Math.PI * r * r;
}
}
import org.springframework.stereotype.Service;
@Service
public class RectangleService {
public Double area(Double r, Double h)
{
return r*h;
}
}
Now if we run the tests, the results are the same. We used constructor injection here, and fortunately, don’t to change our JUnit test case.
But there is another way to inject the beans of square, circle, and rectangle services: field injection. If we use that, then our JUnit test case will need some minor changes.
We won’t go into the discussion of which injection mechanism is better, as that’s not in the scope of the article. But we can say this: No matter what type of mechanism you use to inject beans, there’s always a way to write JUnit tests for it.
In the case of field injection, the code goes something like this:
@Component
public class CalculateArea {
@Autowired
SquareService squareService;
@Autowired
RectangleService rectangleService;
@Autowired
CircleService circleService;
public Double calculateArea(Type type, Double... r )
{
// (same implementation as before)
}
}
Note: Since we are using field injection, there is no need for a parameterized constructor, so the object is created using the default one and values are set using the field injection mechanism.
The code for our service classes remains the same as above, but the code for the test class is as follows:
public class CalculateAreaTest {
@Mock
RectangleService rectangleService;
@Mock
SquareService squareService;
@Mock
CircleService circleService;
@InjectMocks
CalculateArea calculateArea;
@Before
public void init()
{
MockitoAnnotations.initMocks(this);
}
@Test
public void calculateRectangleAreaTest()
{
Mockito.when(rectangleService.area(5.0d,4.0d)).thenReturn(20d);
Double calculatedArea = this.calculateArea.calculateArea(Type.RECTANGLE, 5.0d, 4.0d);
Assert.assertEquals(new Double(20d),calculatedArea);
}
}
A few things go differently here: not the basics, but the way we achieve it.
First, the way we mock our objects: We use @Mock
annotations along with initMocks()
to create mocks. Second, we inject mocks into the actual object using @InjectMocks
along with initMocks()
.
This is just done to reduce the number of lines of code.
What Are Test Runners, and What Types of Runners Are There?
In the above sample, the basic runner that is used to run all the tests is BlockJUnit4ClassRunner
which detects all the annotations and run all the tests accordingly.
If we want some more functionality then we may write a custom runner. For example, in the above test class, if we want to skip the line MockitoAnnotations.initMocks(this);
then we could use a different runner that is built on top of
BlockJUnit4ClassRunner
, e.g. MockitoJUnitRunner
.
Using MockitoJUnitRunner
, we don’t even need to initialize mocks and inject them. That will be done by MockitoJUnitRunner
itself just by reading annotations.
(There’s also SpringJUnit4ClassRunner
, which initializes the ApplicationContext
needed for Spring integration testing—just like an ApplicationContext
is created when a Spring application starts. This we’ll cover later.)
Partial Mocking
When we want an object in the test class to mock some method(s), but also call some actual method(s), then we need partial mocking. This is achieved via @Spy
in JUnit.
Unlike using @Mock
, with @Spy
, a real object is created, but the methods of that object can be mocked or can be actually called—whatever we need.
For example, if the area
method in the class RectangleService
calls an extra method log()
and we actually want to print that log, then the code changes to something like the below:
@Service
public class RectangleService {
public Double area(Double r, Double h)
{
log();
return r*h;
}
public void log() {
System.out.println("skip this");
}
}
If we change the @Mock
annotation of rectangleService
to @Spy
, and also make some code changes as shown below then in the results we would actually see the logs getting printed, but the method area()
will be mocked. That is, the original function is run solely for its side-effects; its return values are replaced by mocked ones.
@RunWith(MockitoJUnitRunner.class)
public class CalculateAreaTest {
@Spy
RectangleService rectangleService;
@Mock
SquareService squareService;
@Mock
CircleService circleService;
@InjectMocks
CalculateArea calculateArea;
@Test
public void calculateRectangleAreaTest()
{
Mockito.doCallRealMethod().when(rectangleService).log();
Mockito.when(rectangleService.area(5.0d,4.0d)).thenReturn(20d);
Double calculatedArea = this.calculateArea.calculateArea(Type.RECTANGLE, 5.0d, 4.0d);
Assert.assertEquals(new Double(20d),calculatedArea);
}
}
How Do We Go About Testing a Controller
or RequestHandler
?
From what we learned above, the test code of a controller for our example would be something like the below:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class AreaController {
@Autowired
CalculateArea calculateArea;
@RequestMapping(value = "api/area", method = RequestMethod.GET)
@ResponseBody
public ResponseEntity calculateArea(
@RequestParam("type") String type,
@RequestParam("param1") String param1,
@RequestParam(value = "param2", required = false) String param2
) {
try {
Double area = calculateArea.calculateArea(
Type.valueOf(type),
Double.parseDouble(param1),
Double.parseDouble(param2)
);
return new ResponseEntity(area, HttpStatus.OK);
}
catch (Exception e)
{
return new ResponseEntity(e.getCause(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@RunWith(MockitoJUnitRunner.class)
public class AreaControllerTest {
@Mock
CalculateArea calculateArea;
@InjectMocks
AreaController areaController;
@Test
public void calculateAreaTest()
{
Mockito
.when(calculateArea.calculateArea(Type.RECTANGLE,5.0d, 4.0d))
.thenReturn(20d);
ResponseEntity responseEntity = areaController.calculateArea("RECTANGLE", "5", "4");
Assert.assertEquals(HttpStatus.OK,responseEntity.getStatusCode());
Assert.assertEquals(20d,responseEntity.getBody());
}
}
Looking at the above controller test code, it works fine, but it has one basic issue: It only tests the method call, not the actual API call. All those test cases where the API parameters and status of API calls need to tested for different inputs are missing.
This code is better:
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;
@RunWith(SpringJUnit4ClassRunner.class)
public class AreaControllerTest {
@Mock
CalculateArea calculateArea;
@InjectMocks
AreaController areaController;
MockMvc mockMvc;
@Before
public void init()
{
mockMvc = standaloneSetup(areaController).build();
}
@Test
public void calculateAreaTest() throws Exception {
Mockito
.when(calculateArea.calculateArea(Type.RECTANGLE,5.0d, 4.0d))
.thenReturn(20d);
mockMvc.perform(
MockMvcRequestBuilders.get("/api/area?type=RECTANGLE¶m1=5¶m2=4")
)
.andExpect(status().isOk())
.andExpect(content().string("20.0"));
}
}
Here we can see how MockMvc
takes the job of performing actual API calls. It also has some special matchers like status()
and content()
which make it easy to validate the content.
Java Integration Testing Using JUnit and Mocks
Now that we know individual units of the code work, let’s conduct some Java integration testing to make sure these units interact with each other as expected.
First, we need to instantiate all the beans, the same stuff that happens at the time of Spring context initialization during application startup.
For this, we define all the beans in a class, let’s say TestConfig.java
:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TestConfig {
@Bean
public AreaController areaController()
{
return new AreaController();
}
@Bean
public CalculateArea calculateArea()
{
return new CalculateArea();
}
@Bean
public RectangleService rectangleService()
{
return new RectangleService();
}
@Bean
public SquareService squareService()
{
return new SquareService();
}
@Bean
public CircleService circleService()
{
return new CircleService();
}
}
Now let’s see how we use this class and write a JUnit integration test:
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {TestConfig.class})
public class AreaControllerIntegrationTest {
@Autowired
AreaController areaController;
MockMvc mockMvc;
@Before
public void init()
{
mockMvc = standaloneSetup(areaController).build();
}
@Test
public void calculateAreaTest() throws Exception {
mockMvc.perform(
MockMvcRequestBuilders.get("/api/area?type=RECTANGLE¶m1=5¶m2=4")
)
.andExpect(status().isOk())
.andExpect(content().string("20.0"));
}
}
A few things change here:
-
@ContextConfiguration(classes = {TestConfig.class})
—this tells the test case where all the bean definitions reside. - Now instead of
@InjectMocks
we use:
@Autowired
AreaController areaController;
Everything else remains the same. If we debug the test, we would see that the code actually runs until the last line of the area()
method in RectangleService
where return r*h
is calculated. In other words, the actual business logic runs.
This does not mean that there is no mocking of method calls or database calls available in integration testing. In the above example, there was no third-party service or database used, hence we did not need to use mocks. In real life, such applications are rare, and we’ll often hit a database or third-party API, or both. In that case, when we create the bean in the TestConfig
class, we don’t create the actual object, but a mocked one, and use it wherever needed.
Bonus: How to Create Large Object Test Data
Often what stops back-end developers in writing unit or integration tests is the test data that we have to prepare for every test.
Normally if the data is small enough, having one or two variables, then it’s easy to just create an object of a test data class and assign some values.
For example, if we are expecting a mocked object to return another object, when a function is called on the mocked object we would do something like this:
Class1 object = new Class1();
object.setVariable1(1);
object.setVariable2(2);
And then in order to use this object, we would do something like this:
Mockito.when(service.method(arguments...)).thenReturn(object);
This is fine in the above JUnit examples, but when the member variables in the above Class1
class keep on increasing, then setting individual fields becomes quite a pain. Sometimes it might even happen that a class has another non-primitive class member defined. Then, creating an object of that class and setting individual required fields further increases the development effort just to accomplish some boilerplate.
The solution is to generate a JSON schema of the above class and add the corresponding data in the JSON file once. Now in the test class where we create the Class1
object, we don’t need to create the object manually. Instead, we read the JSON file and, using ObjectMapper
, map it into the required Class1
class:
ObjectMapper objectMapper = new ObjectMapper();
Class1 object = objectMapper.readValue(
new String(Files.readAllBytes(
Paths.get("src/test/resources/"+fileName))
),
Class1.class
);
This is a one-time effort of creating a JSON file and adding values to it. Any new tests after that can use a copy of that JSON file with fields changed according to the needs of the new test.
JUnit Basics: Multiple Approaches and Transferable Skills
It’s clear that there are many ways to write Java unit tests depending upon how we choose to inject beans. Unfortunately, most articles on the topic tend to assume there’s only one way, so it’s easy to get confused, especially when working with code that was written under a different assumption. Hopefully, our approach here saves developers time in figuring out the correct way to mock and which test runner to use.
Irrespective of the language or framework we use—perhaps even any new version of Spring or JUnit—the conceptual base remains the same as explained in the above JUnit tutorial. Happy testing!
Further Reading on the Toptal Blog:
Understanding the basics
How do you write a unit test in Java?
JUnit is the most famous framework for writing unit tests in Java. With JUnit testing in Java, you write test methods that call the actual methods to be tested. The test case verifies the behavior of the code by asserting the return value against the expected value, given the parameters passed.
What is the best unit testing framework for Java?
The majority of Java developers agree that JUnit is the best unit testing framework. JUnit testing in Java has been the de facto standard since 1997, and certainly has the largest amount of support compared to other Java unit testing frameworks. JUnit is also useful for Java integration tests.
What is a unit test in programming?
In unit testing, individual units (often, object methods are considered a “unit”) are tested in an automated fashion.
Why do we use JUnit testing?
JUnit testing is used to test the behavior of methods inside classes we have written. We test a method for the expected results and sometimes exception-throwing cases—whether the method is able to handle the exceptions in the way we want.
What is the use of JUnit?
JUnit is a framework which provides many different classes and methods to write unit tests easily.
Is JUnit open source?
Yes, JUnit is an open-source project, maintained by many active developers.
Why is JUnit important?
JUnit reduces the boilerplate that developers need to use when writing unit tests.
Who invented JUnit?
Kent Beck and Erich Gamma initially created JUnit. Nowadays, the open-source project has over a hundred contributors.
Nikhil Bansal
Gurgaon, Haryana, India
Member since November 27, 2018
About the author
Nikhil (BTech) has automated finance sector code across everything from J2EE to Elasticsearch to Kafka. And, of course, all things AWS.
Expertise
PREVIOUSLY AT