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 的测试支持。`SpringRunner`是`SpringJUnit4ClassRunner`的新名称,它更容易阅读。
  • `@SpringBootTest`表示“使用 Spring Boot 的支持引导”(例如,加载`application.properties`并提供所有 Spring Boot 的优点)。
  • `webEnvironment`属性允许为测试配置特定的“Web 环境”。你可以使用`MOCK`servlet 环境或在`RANDOM_PORT`或`DEFINED_PORT`上运行的真实 HTTP 服务器启动测试。
  • 如果我们想加载特定配置,可以使用`@SpringBootTest`的`classes`属性。在这个例子中,我们省略了`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");
    }

}

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

模拟和间谍

当你开始测试真实系统时,你经常会发现模拟特定 bean 很方便。模拟的常见场景包括模拟在运行测试时无法使用的服务,或测试在实时系统中难以触发的故障场景。

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

@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社区中所有即将举行的活动。

查看全部