Back-end11 minute read

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?


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

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?


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Nikhil Bansal
Verified Expert in Engineering

Nikhil (BTech) has automated finance sector code across everything from J2EE to Elasticsearch to Kafka. And, of course, all things AWS.

PREVIOUSLY AT

Expedia Group
Share

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 the rectangleService object’s area method is called with the specified parameters, then return 20d.

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 type CalculateArea
  • @Autowired: Searches for the beans rectangleService, squareService, and circleService and injects them into the bean calculatedArea

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&param1=5&param2=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&param1=5&param2=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.

  • 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.

  • In unit testing, individual units (often, object methods are considered a “unit”) are tested in an automated fashion.

  • 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.

  • JUnit is a framework which provides many different classes and methods to write unit tests easily.

  • Yes, JUnit is an open-source project, maintained by many active developers.

  • JUnit reduces the boilerplate that developers need to use when writing unit tests.

  • Kent Beck and Erich Gamma initially created JUnit. Nowadays, the open-source project has over a hundred contributors.

Hire a Toptal expert on this topic.
Hire Now
Nikhil Bansal

Nikhil Bansal

Verified Expert in Engineering

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.

authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

PREVIOUSLY AT

Expedia Group

World-class articles, delivered weekly.

By entering your email, you are agreeing to our privacy policy.

World-class articles, delivered weekly.

By entering your email, you are agreeing to our privacy policy.

Join the Toptal® community.