In the world of rapid software development, Java developers rely heavily on automated testing to ensure that code changes don’t introduce regressions. While there are many approaches to automated testing, understanding how they relate to each other can be challenging.
The move from traditional waterfall models to Agile and DevOps methodologies has led to a significant increase in the pace of software delivery. Code changes are often deployed to production environments soon after they are made. This rapid deployment cycle necessitates a high level of confidence in the quality and stability of the code.
Automated regression testing provides this confidence. While API-level tests are crucial, two fundamental types of tests form the bedrock of regression testing:
- Unit testing, focusing on verifying the functionality of the smallest code units like functions or procedures, with or without inputs and outputs.
- Integration testing, aiming to confirm that individual units of code function correctly when used together.
Numerous testing frameworks exist for various programming languages. This article focuses on unit and integration testing for Java web applications built with the Spring framework.
Modern applications, particularly in enterprise applications, are highly complex, with a single method often interacting with multiple methods across different classes. Unit testing such methods requires a way to simulate the data returned by these external calls without actually executing them. This is where mocking comes into play.
Let’s dive into Java unit testing within the Spring framework using JUnit, starting with the concept of mocking.
What is Mocking and When Is It Used?
Imagine a class, CalculateArea, with a method calculateArea(Type type, Double... args). This method determines the area of a shape based on its type (circle, square, or rectangle).
In a typical application without dependency injection, the code might look like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| 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");
}
}
}
|
1
2
3
4
5
6
7
| public class SquareService {
public Double area(double r)
{
return r * r;
}
}
|
1
2
3
4
5
6
7
| public class RectangleService {
public Double area(Double r, Double h)
{
return r * h;
}
}
|
1
2
3
4
5
6
7
| public class CircleService {
public Double area(Double r)
{
return Math.PI * r * r;
}
}
|
1
2
3
4
| public enum Type {
RECTANGLE,SQUARE,CIRCLE;
}
|
To unit-test the calculateArea() method effectively, the focus should be on verifying the switch cases and exception handling logic. The goal is not to test the shape services’ accuracy, as unit testing aims to isolate and validate the logic of a specific function, not its dependencies.
Therefore, we would mock the values returned by the shape service functions (e.g., rectangleService.area()) and test the calling function (CalculateArea.calculateArea()) based on these simulated values.
A basic test case for the rectangle service, confirming that calculateArea() calls rectangleService.area() with the expected parameters, would resemble this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| 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 crucial lines in this test case are:
rectangleService = Mockito.mock(RectangleService.class);—This line generates a mock object, a simulated version of the actual RectangleService.Mockito.when(rectangleService.area(5.0d,4.0d)).thenReturn(20d);—This line instructs the mock to return a specific value (20d) when the area method of the mocked rectangleService is called with the defined parameters (5.0d, 4.0d).
Let’s explore how this scenario changes within a Spring application context.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| 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, two annotations are used for Spring’s dependency injection mechanism:
@Component: Designates CalculateArea as a bean, making it manageable by Spring.@Autowired: Directs Spring to locate beans of type rectangleService, squareService, and circleService and inject them into the calculatedArea bean.
Similar bean definitions would exist for other classes:
1
2
3
4
5
6
7
8
9
10
| import org.springframework.stereotype.Service;
@Service
public class SquareService {
public Double area(double r)
{
return r*r;
}
}
|
1
2
3
4
5
6
7
8
9
10
| import org.springframework.stereotype.Service;
@Service
public class CircleService {
public Double area(Double r)
{
return Math.PI * r * r;
}
}
|
1
2
3
4
5
6
7
8
9
10
| import org.springframework.stereotype.Service;
@Service
public class RectangleService {
public Double area(Double r, Double h)
{
return r*h;
}
}
|
Running the tests within this Spring context would yield the same results. In this case, constructor injection is used, and the JUnit test case remains unchanged.
However, Spring offers another mechanism for bean injection: field injection. Using this approach would require slight modifications to the JUnit test case.
A detailed comparison of constructor and field injection is beyond this article’s scope. It’s important to note that JUnit tests can be adapted to work with both mechanisms.
When using field injection, the code would look like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| @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: With field injection, there’s no need for a parameterized constructor. The object is created using the default constructor, and values are populated through field injection.
The service class code remains unchanged, but the test class requires adjustments:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| 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);
}
}
|
While the fundamental concepts remain the same, the implementation differs slightly. Mocks are created using @Mock annotations in conjunction with initMocks(), and @InjectMocks along with initMocks() is employed to inject these mocks into the actual object under test. These changes primarily serve to reduce code verbosity.
Test Runners: An Overview
The example above uses the default JUnit runner, BlockJUnit4ClassRunner, which identifies annotations and executes tests accordingly.
For more specialized functionality, custom runners can be employed. For instance, to eliminate the line MockitoAnnotations.initMocks(this);, a runner based on BlockJUnit4ClassRunner, such as MockitoJUnitRunner, can be utilized.
MockitoJUnitRunner handles mock initialization and injection automatically based on annotations. Another useful runner is SpringJUnit4ClassRunner, which initializes the Spring ApplicationContext necessary for integration testing, similar to how it’s done during application startup. This will be explored later.
Partial Mocking with @Spy
In scenarios where a test requires mocking some methods of an object while invoking others directly, partial mocking is used. JUnit facilitates this through the @Spy annotation.
Unlike @Mock, which creates a completely mocked object, @Spy creates a real object but allows for selective mocking of its methods.
For example, if the area method in RectangleService calls a logging method (log()), and we want to observe the actual log output during testing, the code would be modified as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
| @Service
public class RectangleService {
public Double area(Double r, Double h)
{
log();
return r*h;
}
public void log() {
System.out.println("skip this");
}
}
|
Changing the @Mock annotation for rectangleService to @Spy and adjusting the code as shown below would result in the logs being printed while the area() method remains mocked. This means the original area() function is executed only for its side effects (logging), while its return value is overridden by the mock.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| @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);
}
}
|
Testing Controllers or RequestHandlers
Building upon the previous concepts, a test for a controller might look like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| 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);
}
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| 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());
}
}
|
While this code functions, it has a limitation: it only tests the method invocation, not the actual API call. It doesn’t cover test cases for validating API parameters, status codes, or responses based on different inputs.
An improved approach would involve using MockMvc:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| 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, MockMvc simulates actual API calls, providing specialized matchers like status() and content() for comprehensive response validation.
Java Integration Testing with JUnit and Mocks
With individual code units validated through unit testing, integration testing ensures that these units interact correctly. This process often involves instantiating all beans, replicating the Spring application context initialization.
One approach is to define bean configurations within a separate class, such as TestConfig.java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| 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();
}
}
|
This configuration class is then used in the integration test:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| 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"));
}
}
|
Key differences in this integration test include:
@ContextConfiguration(classes = {TestConfig.class})—Specifies the location of bean definitions.@Autowired is used directly for bean injection instead of @InjectMocks:
1
2
| @Autowired
AreaController areaController;
|
Debugging this test would reveal that the code executes until the final line of the area() method in RectangleService, where the actual business logic (return r*h) resides.
Integration testing doesn’t preclude mocking. In scenarios involving external services or databases, mocked objects can be defined within the TestConfig class and injected as needed.
Bonus: Managing Test Data for Large Objects
A common challenge in back-end testing is managing test data, especially for complex objects. While simple objects with few fields can be created and populated manually, this becomes cumbersome for larger objects.
For instance, if a mocked object needs to return a complex object (Class1) with numerous fields, manually setting each field becomes tedious:
1
2
3
| Class1 object = new Class1();
object.setVariable1(1);
object.setVariable2(2);
|
Using this object in the test would involve accessing its fields:
1
| Mockito.when(service.method(arguments...)).thenReturn(object);
|
As the number of fields in Class1 increases, or if it contains nested non-primitive types, maintaining this test data becomes unwieldy.
A more scalable approach is to create a JSON schema for Class1 and store corresponding test data in a JSON file. This JSON data can then be read and deserialized into a Class1 object using an ObjectMapper within the test class:
1
2
3
4
5
6
7
| ObjectMapper objectMapper = new ObjectMapper();
Class1 object = objectMapper.readValue(
new String(Files.readAllBytes(
Paths.get("src/test/resources/"+fileName))
),
Class1.class
);
|
This one-time effort of creating the JSON file simplifies test data management, allowing for easy modification and reuse for different test scenarios.
Conclusion
This article highlighted various approaches to writing Java unit tests within the Spring framework, emphasizing the importance of understanding different injection mechanisms and test runners. The key takeaway is that while implementation details might differ, the fundamental principles of unit and integration testing remain consistent across languages and frameworks.
By grasping the concepts of mocking, partial mocking, test runners, and test data management, developers can write robust and maintainable tests, ensuring the reliability and quality of their Java applications.