1. 概述

在本教程中,我们将学习如何在 JUnit4 中使用 Parameterized 测试运行器来参数化一个 Spring 集成测试。

2. SpringJUnit4ClassRunner

SpringJUnit4ClassRunner 是 JUnit4 中 ClassRunner 的一个实现,它将 Spring 的 TestContextManager 嵌入到 JUnit 测试中。

TestContextManager 是 Spring 测试框架的入口点,负责管理 Spring 应用上下文(ApplicationContext)以及依赖注入。因此,SpringJUnit4ClassRunner 让我们可以方便地为 Spring 组件(如 Controller、Repository)编写集成测试。

例如,我们可以为一个 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"));
    }
}

如上所示,这个测试会模拟访问 /role/John 接口,并验证返回是否为 "ADMIN"。

如果我们想测试其他用户名/角色组合,比如 /role/Doe 返回 "EMPLOYEE",就需要再写一个新测试:

@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"));
}

这样写的话,当测试组合较多时,代码会显得冗长,重复度高。为了解决这个问题,我们来看看如何使用 JUnit 的 Parameterized 测试运行器。

3. 使用 Parameterized

3.1. 定义参数

JUnit 提供了 Parameterized 测试运行器,允许我们为一个测试方法提供多组输入数据,从而避免重复代码。

示例如下:

@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;
    }

    // ...
}

上面的代码中,我们通过 @Parameters 注解定义了两组测试参数,每组参数分别对应 namerole 两个字段。

⚠️ 但这里有个问题:JUnit 不允许一个测试类使用多个运行器。也就是说,我们不能同时使用 @RunWith(Parameterized.class)SpringJUnit4ClassRunner。这就需要我们手动集成 Spring 的测试支持。

3.2. 手动初始化 TestContextManager

Spring 提供了 TestContextManager 类来管理测试上下文。我们可以在测试类中手动初始化它:

@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();
    }

    @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));
    }
}

✅ 该方法可行,但 不推荐。因为手动管理 TestContextManager 容易出错,也不够优雅。

3.3. 使用 SpringClassRule 与 SpringMethodRule

Spring 推荐使用 SpringClassRuleSpringMethodRule 来替代手动初始化。它们是 JUnit 的 TestRule 实现,可以替代传统的 @Before, @After 等注解进行测试上下文管理。

  • SpringClassRule: 提供类级别的上下文初始化
  • SpringMethodRule: 提供方法级别的上下文初始化

示例代码如下:

@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();

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

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

    @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));
    }
}

✅ 这种方式更推荐使用,因为 Spring 官方明确建议使用 SpringClassRule + SpringMethodRule 来替代手动初始化。

4. 总结

本文介绍了两种在 JUnit4 中使用 Parameterized 测试运行器进行 Spring 集成测试的方法:

  • ✅ 手动初始化 TestContextManager(不推荐)
  • ✅ 使用 SpringClassRule + SpringMethodRule(推荐)

虽然本文只讨论了 Parameterized 运行器,但这些方法也适用于其他 JUnit 测试运行器。使用参数化测试可以大幅减少重复测试代码,提高测试覆盖率和可维护性。

所有示例代码已上传至 GitHub:点击访问


原始标题:Using SpringJUnit4ClassRunner with Parameterized | Baeldung