Spring Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial, we'll see how to parameterize a Spring integration test implemented in JUnit4 with a Parameterized JUnit test runner.

2. SpringJUnit4ClassRunner

SpringJUnit4ClassRunner is an implementation of JUnit4's ClassRunner that embeds Spring's TestContextManager into a JUnit test.

TestContextManager is the entry point into the Spring TestContext framework and therefore manages the access to Spring ApplicationContext and dependency injection in a JUnit test class. Thus, SpringJUnit4ClassRunner enables developers to implement integration tests for Spring components like controllers and repositories.

For example, we can implement an integration test for our RestController:

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes = WebConfig.class)
public class RoleControllerIntegrationTest {

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    private static final String CONTENT_TYPE = "application/text;charset=ISO-8859-1";

    @Before
    public void setup() throws Exception {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
    }

    @Test
    public void givenEmployeeNameJohnWhenInvokeRoleThenReturnAdmin() throws Exception {
        this.mockMvc.perform(MockMvcRequestBuilders
          .get("/role/John"))
          .andDo(print())
          .andExpect(MockMvcResultMatchers.status().isOk())
          .andExpect(MockMvcResultMatchers.content().contentType(CONTENT_TYPE))
          .andExpect(MockMvcResultMatchers.content().string("ADMIN"));
    }
}

As can be seen from the test, our Controller accepts a user name as a path parameter and returns the user role accordingly.

Now, in order to test this REST service with a different user name/role combination, we would have to implement a new test:

@Test
public void givenEmployeeNameDoeWhenInvokeRoleThenReturnEmployee() throws Exception {
    this.mockMvc.perform(MockMvcRequestBuilders
      .get("/role/Doe"))
      .andDo(print())
      .andExpect(MockMvcResultMatchers.status().isOk())
      .andExpect(MockMvcResultMatchers.content().contentType(CONTENT_TYPE))
      .andExpect(MockMvcResultMatchers.content().string("EMPLOYEE"));
}

This can quickly get out of hand for services where a large number of input combinations are possible.

To avoid this kind of repetition in our test classes, let's see how to use Parameterized for implementing JUnit tests that accept multiple inputs.

3. Using Parameterized

3.1. Defining Parameters

Parameterized is a custom JUnit test runner that allows us to write a single test case and have it run against multiple input parameters:

@RunWith(Parameterized.class)
@WebAppConfiguration
@ContextConfiguration(classes = WebConfig.class)
public class RoleControllerParameterizedIntegrationTest {

    @Parameter(value = 0)
    public String name;

    @Parameter(value = 1)
    public String role;

    @Parameters
    public static Collection<Object[]> data() {
        Collection<Object[]> params = new ArrayList();
        params.add(new Object[]{"John", "ADMIN"});
        params.add(new Object[]{"Doe", "EMPLOYEE"});

        return params;
    }

    //...
}

As shown above, we used the @Parameters annotation to prepare the input parameters to be injected into the JUnit test. We also provided the mapping of these values in the @Parameter fields name and role.

But now, we have another problem to solve — JUnit doesn't allow multiple runners in one JUnit test class. This means we can't take advantage of SpringJUnit4ClassRunner to embed the TestContextManager into our test class. We'll have to find another way to embed TestContextManager.

Fortunately, Spring provides a couple of options for achieving this. We'll discuss these in the following sections.

3.2. Initializing the TestContextManager Manually

The first option is quite simple, as Spring allows us to initialize TestContextManager manually:

@RunWith(Parameterized.class)
@WebAppConfiguration
@ContextConfiguration(classes = WebConfig.class)
public class RoleControllerParameterizedIntegrationTest {

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    private TestContextManager testContextManager;

    @Before
    public void setup() throws Exception {
        this.testContextManager = new TestContextManager(getClass());
        this.testContextManager.prepareTestInstance(this);

        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
    }

    //...
}

Notably, in this example, we used the Parameterized runner instead of the SpringJUnit4ClassRunner. Next, we initialized the TestContextManager in the setup() method.

Now, we can implement our parameterized JUnit test:

@Test
public void givenEmployeeNameWhenInvokeRoleThenReturnRole() throws Exception {
    this.mockMvc.perform(MockMvcRequestBuilders
      .get("/role/" + name))
      .andDo(print())
      .andExpect(MockMvcResultMatchers.status().isOk())
      .andExpect(MockMvcResultMatchers.content().contentType(CONTENT_TYPE))
      .andExpect(MockMvcResultMatchers.content().string(role));
}

JUnit will execute this test case twice — once for each set of inputs we defined using the @Parameters annotation.

3.3. SpringClassRule and SpringMethodRule

Generally, it is not recommended to initialize the TestContextManager manually. Instead, Spring recommends using SpringClassRule and SpringMethodRule.

SpringClassRule implements JUnit's TestRule — an alternate way to write test cases. TestRule can be used to replace the setup and cleanup operations that were previously done with @Before, @BeforeClass, @After, and @AfterClass methods.

SpringClassRule embeds class-level functionality of TestContextManager in a JUnit test class. It initializes the TestContextManager and invokes the setup and cleanup of the Spring TestContext. Therefore, it provides dependency injection and access to the ApplicationContext.

In addition to SpringClassRule, we must also use SpringMethodRule. which provides the instance-level and method-level functionality for TestContextManager.

SpringMethodRule is responsible for the preparation of the test methods. It also checks for test cases that are marked for skipping and prevents them from running.

Let's see how to use this approach in our test class:

@RunWith(Parameterized.class)
@WebAppConfiguration
@ContextConfiguration(classes = WebConfig.class)
public class RoleControllerParameterizedClassRuleIntegrationTest {
    @ClassRule
    public static final SpringClassRule scr = new SpringClassRule();

    @Rule
    public final SpringMethodRule smr = new SpringMethodRule();

    @Before
    public void setup() throws Exception {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
    }

    //...
}

4. Conclusion

In this article, we discussed two ways to implement Spring integration tests using the Parameterized test runner instead of SpringJUnit4ClassRunner. We saw how to initialize TestContextManager manually, and we saw an example using SpringClassRule with SpringMethodRule, the approach recommended by Spring.

Although we only discussed the Parameterized runner in this article, we can actually use either of these approaches with any JUnit runner to write Spring integration tests.

As usual, all the example code is available over on GitHub.

Spring bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
Comments are closed on this article!