Spring Boot 1.4 中的测试改进

工程 | Phil Webb | 2016年4月15日 | ...

在 Pivotal 工作的好处之一是他们有一个出色的敏捷开发部门,名为 Pivotal Labs。Labs 中的团队是精益和 XP 软件方法论(如结对编程和测试驱动开发)的忠实拥护者。他们对测试的热爱对 Spring Boot 1.4 产生了特别大的影响,因为我们开始收到有关可以改进之处的绝佳反馈。这篇博文重点介绍了最新 M2 版本中新增的一些测试功能。

不使用 Spring 进行测试

测试任何 Spring @Component 的最简单方法是完全不引入 Spring!最好始终尝试组织代码,以便可以直接实例化和测试类。通常这归结为几点:

  • 通过清晰的关注点分离来组织代码,以便可以单独对各个部分进行单元测试。TDD 是实现此目标的好方法。
  • 使用构造函数注入以确保可以之间实例化对象。不要使用字段注入,因为它只会使编写测试更加困难。

使用 Spring Framework 4.3,编写使用构造函数注入的组件非常容易,因为您不再需要使用 @Autowired。只要您有一个构造函数,Spring 就会隐式地将其视为自动注入目标。

@Component
public class MyComponent {
    
    private final SomeService service;

    public MyComponent(SomeService service) {
        this.service = service;
    }

} 

现在,测试 MyComponent 就像直接创建它并调用一些方法一样简单。

@Test
public void testSomeMethod() {
    SomeService service = mock(SomeService.class);
    MyComponent component = new MyComponent(service);
    // setup mock and class component methods
}

Spring Boot 1.3 回顾

当然,通常您需要向上移动一点,开始编写确实涉及 Spring 的集成测试。幸运的是,Spring Framework 提供了 spring-test 模块来提供帮助,不幸的是,在使用 Spring Boot 1.3 时有许多不同的方法可以使用它。

您可能正在将 @ContextConfiguration 注解与 SpringApplicationContextLoader 结合使用。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=MyApp.class, loader=SpringApplicationContextLoader.class)
public class MyTest {

    // ...

}

您可能已经选择了 @SpringApplicationConfiguration

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(MyApp.class)
public class MyTest {

    // ...

}

您可能已经将其中任何一个与 @IntegrationTest 结合使用。

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(MyApp.class)
@IntegrationTest
public class MyTest {

    // ...

}

或者与 @WebIntegrationTest(或可能 @IntegrationTest + @WebAppConfiguration)结合使用。

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(MyApp.class)
@WebIntegrationTest
public class MyTest {

    // ...

}

您还可以将服务器运行在随机端口(@WebIntegrationTest(randomPort=true))以及添加属性(使用 @IntegrationTest("myprop=myvalue")@TestPropertySource(properties="myprop=myvalue"))的混合体。

选择太多了!

Spring Boot 1.4 简化

有了 Spring Boot 1.4,事情应该会变得更简单。有一个单一的 @SpringBootTest 注解可供常规测试使用,以及一些用于测试应用程序“切片”的专用变体(稍后将详细介绍)。

一个典型的 Spring Boot 1.4 集成测试将如下所示:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)
public class MyTest {

    // ...
    
}

以下是发生情况的分解:

  • @RunWith(SpringRunner.class) 告诉 JUnit 使用 Spring 的测试支持来运行。SpringRunnerSpringJUnit4ClassRunner 的新名称,它只是看起来更赏心悦目一些。
  • @SpringBootTest 表示“使用 Spring Boot 的支持进行引导”(例如,加载 application.properties 并给我所有的 Spring Boot 功能)。
  • webEnvironment 属性允许为测试配置特定的“Web 环境”。您可以启动带有 MOCK Servlet 环境的测试,或者在 RANDOM_PORTDEFINED_PORT 上运行真实 HTTP 服务器的测试。
  • 如果我们想加载特定的配置,我们可以使用 @SpringBootTestclasses 属性。在此示例中,我们省略了 classes,这意味着测试将首先尝试加载任何内部类中的 @Configuration,如果失败,它将搜索您的主 @SpringBootApplication 类。

@SpringBootTest 注解还有一个 properties 属性,可用于指定应在 Environment 中定义的任何其他属性。属性现在的加载方式与 Spring 的常规 @TestPropertySource 注解完全相同。

这是一个更具体的例子,它实际命中了一个真实的 REST 端点:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)
public class MyTest {
    
    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void test() {
        this.restTemplate.getForEntity(
            "/{username}/vehicle", String.class, "Phil");
    }

}

请注意,TestRestTemplate 现在作为 bean 在使用 @SpringBootTest 时可用。它已预先配置为将相对路径解析到 https://:${local.server.port}。我们还可以使用 @LocalServerPort 注解将服务器实际运行的端口注入到测试字段中。

模拟和间谍

当您开始测试真实系统时,您会发现模拟特定 bean 很有帮助。模拟的常见场景包括模拟在运行测试时无法使用的服务,或测试在活动系统中难以触发的失败场景。

使用 Spring Boot 1.4,您可以轻松创建 Mockito 模拟,这些模拟可以替换现有 bean,或创建新的 bean。

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class SampleTestApplicationWebIntegrationTests {

    @Autowired
    private TestRestTemplate restTemplate;

    @MockBean
    private VehicleDetailsService vehicleDetailsService;

    @Before
    public void setup() {
        given(this.vehicleDetailsService.
            getVehicleDetails("123")
        ).willReturn(
            new VehicleDetails("Honda", "Civic"));
    }

    @Test
    public void test() {
        this.restTemplate.getForEntity("/{username}/vehicle", 
            String.class, "sframework");
    }

}

在此示例中,我们正在:

  • VehicleDetailsService 创建 Mockito 模拟。
  • 将其作为 bean 注入到 ApplicationContext 中。
  • 将其注入到测试字段中。
  • setup 方法中模拟行为。
  • 触发将最终调用模拟的操作。

模拟会在测试之间自动重置。它们也是 Spring Test 使用的缓存键的一部分(因此无需添加 @DirtiesContext)。

间谍以类似的方式工作。只需用 @SpyBean 标注一个测试字段,即可让间谍包装 ApplicationContext 中的任何现有 bean。

JSON 断言

如果您使用 spring-boot-starter-test POM 导入测试依赖项,从 1.4 开始,您将获得出色的 AssertJ 库。AssertJ 提供了一个流畅的断言 API,它取代了 JUnit 相当基础的 org.junit.Assert 类。如果您以前没见过,一个基本的 AssertJ 调用大致如下所示:

assertThat(library.getName()).startsWith("Spring").endsWith("Boot");

Spring Boot 1.4 提供了扩展断言,您可以使用它们来检查 JSON 的序列化和反序列化。JSON 测试器可用于 Jackson 和 Gson。

public class VehicleDetailsJsonTests {

    private JacksonTester<VehicleDetails> json;

    @Before
    public void setup() {
        ObjectMapper objectMapper = new ObjectMapper(); 
        // Possibly configure the mapper
        JacksonTester.initFields(this, objectMapper);
    }

    @Test
    public void serializeJson() {
        VehicleDetails details = 
            new VehicleDetails("Honda", "Civic");

        assertThat(this.json.write(details))
            .isEqualToJson("vehicledetails.json");

        assertThat(this.json.write(details))
            .hasJsonPathStringValue("@.make");

        assertThat(this.json.write(details))
            .extractingJsonPathStringValue("@.make")
            .isEqualTo("Honda");
    }

    @Test
    public void deserializeJson() {
        String content = "{\"make\":\"Ford\",\"model\":\"Focus\"}";

        assertThat(this.json.parse(content))
            .isEqualTo(new VehicleDetails("Ford", "Focus"));

        assertThat(this.json.parseObject(content).getMake())
            .isEqualTo("Ford");
    }

}

JSON 比较实际上是使用 JSONassert 执行的,因此只需要匹配 JSON 的逻辑结构。您还可以在上面的示例中看到如何使用 JsonPath 表达式来测试或提取数据。

测试应用程序切片

Spring Boot 的自动配置功能非常适合配置应用程序运行所需的所有内容。不幸的是,完全自动配置有时对测试来说可能有点过度。有时您只想配置应用程序的“切片”——Jackson 配置正确吗?我的 MVC 控制器是否返回正确的状态码?我的 JPA 查询会运行吗?

借助 Spring Boot 1.4,这些常见场景现在易于测试。我们也使其更容易构建您自己的注解,这些注解仅应用于您需要的自动配置类。

测试 JPA 切片

要测试应用程序的 JPA 切片(Hibernate + Spring Data),您可以使用 @DataJpaTest 注解。@DataJpaTest 将:

  • 配置内存数据库。
  • 自动配置 Hibernate、Spring Data 和 DataSource。
  • 执行 @EntityScan
  • 启用 SQL 日志记录。

典型的测试如下所示:

@RunWith(SpringRunner.class)
@DataJpaTest
public class UserRepositoryTests {

    @Autowired
    private TestEntityManager entityManager;

    @Autowired
    private UserRepository repository;

    @Test
    public void findByUsernameShouldReturnUser() {
        this.entityManager.persist(new User("sboot", "123"));
        User user = this.repository.findByUsername("sboot");
        
        assertThat(user.getUsername()).isEqualTo("sboot");
        assertThat(user.getVin()).isEqualTo("123");
    }

}

上面的测试中的 TestEntityManager 由 Spring Boot 提供。它是标准 JPA EntityManager 的替代品,提供了在编写测试时常用的方法。

测试 Spring MVC 切片

要测试应用程序的 Spring MVC 切片,您可以使用 @WebMvcTest 注解。这将:

  • 自动配置 Spring MVC、Jackson、Gson、消息转换器等。
  • 加载相关组件(@Controller@RestController@JsonComponent 等)。
  • 配置 MockMVC。

这是一个测试单个控制器的典型示例:

@RunWith(SpringRunner.class)
@WebMvcTest(UserVehicleController.class)
public class UserVehicleControllerTests {

    @Autowired
    private MockMvc mvc;

    @MockBean
    private UserVehicleService userVehicleService;

    @Test
    public void getVehicleShouldReturnMakeAndModel() {
        given(this.userVehicleService.getVehicleDetails("sboot"))
            .willReturn(new VehicleDetails("Honda", "Civic"));

        this.mvc.perform(get("/sboot/vehicle")
            .accept(MediaType.TEXT_PLAIN))
            .andExpect(status().isOk())
            .andExpect(content().string("Honda Civic"));
    }

}

如果您更喜欢 HtmlUnit,您也可以使用 WebClient 而不是 MockMvc。如果 Selenium 是您的首选,您可以切换到 WebDriver

测试 JSON 切片

如果您需要测试 JSON 序列化是否按预期工作,您可以使用 @JsonTest。这将:

  • 自动配置 Jackson 和/或 Gson。
  • 添加您定义的任何 Module@JsonComponent bean。
  • 触发任何 JacksonTesterGsonTester 字段的初始化。

这是一个例子:

@RunWith(SpringRunner.class)
@JsonTest
public class VehicleDetailsJsonTests {

    private JacksonTester<VehicleDetails> json;

    @Test
    public void serializeJson() {
        VehicleDetails details = new VehicleDetails(
            "Honda", "Civic");

        assertThat(this.json.write(details))
            .extractingJsonPathStringValue("@.make")
            .isEqualTo("Honda");
    }

}

总结

如果您想尝试 Spring Boot 1.4 中的新测试功能,您可以从 http://repo.spring.io/snapshot/ 获取 M2 版本。在 GitHub 上还有一个示例项目,以及更新的文档。如果您对我们应该支持的附加“切片”有任何建议,或者您希望看到的任何进一步改进,请提出一个问题

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看所有