集成测试可能很慢且不可靠,因为它们依赖于系统中过多的组件。 在某种程度上,这是不可避免的:这里的集成测试是为了验证系统的每个部分如何与其他内部或外部组件一起玩。
但是,我们可以通过仅分解所需的依赖关系而不是整个系统来改进某些集成测试。 让我们想象一个依赖于数据库,第三方REST API和消息队列的应用程序:
现在假设我们希望集成测试验证仅包含对REST API的调用但不包含对数据库或消息队列的调用的行为。 举一个具体的例子,假设我们要检查REST客户端是否正确配置为3秒钟后超时。
为此,我们需要的是一个小型Controller
,该Controller
将在返回答案到REST客户端之前等待,以模拟REST API。 等待时间将作为参数传递给查询字符串。
@Profile("restTemplateTimeout")
@RestController
@RequestMapping(value = "/test")
public class DelayedWebServerController {@RequestMapping(value = "/delayRestTemplate", method = GET)public String answerWithDelay(@RequestParam Long waitTimeMs) {if (waitTimeMs > 0) {try {Thread.sleep(waitTimeMs);} catch (InterruptedException e) {throw new RuntimeException(e);}}return "Delayed Result";}}
@Profile
批注用于什么? 如果我们将此控制器注入到我们的标准应用程序上下文中,则有几个缺点:
- 测试将会很慢:我们只需要启动一个控制器,而不是整个控制器
- 我们的控制器将由Spring拾取并注入其他所有集成测试中,从而减慢每个集成测试的速度,并可能踩到另一个测试的脚趾
更好的选择是启动一个最小的Spring Boot应用程序,仅暴露我们的DelayedWebServerController
。 我们还将告诉Spring Boot仅扫描我们感兴趣的软件包,并排除与持久性相关的自动配置,因为我们不需要它来启动控制器。 这是在类似以下的Configuration
类中完成的:
@Profile("restTemplateTimeout")
@Configuration
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class})
@ComponentScan(basePackages = "my.application.resttemplate.timeout")
public class DelayedWebServerConfiguration {//The class is empty and only used to support the annotations
}
Spring上下文配置可能会引起混乱,让我们一个接一个地查看批注:
-
@Profile
:这告诉Spring,只有在restTemplateTimeout
配置文件处于活动状态时才应使用此配置。 在本文的进一步内容中,我们将看到如何为特定的集成测试启用此配置文件。 正是这个注释防止配置被其他不相关的集成测试所采用。 请注意,我们的DelayedWebServerController
具有相同的注释。 -
@Configuration
:标准注释,用于告诉Spring这是一个上下文配置类。 -
@EnableAutoConfiguration
:在这里,我们禁用了某些不需要进行特定测试的Spring Boot“魔术” -
@ComponentScan
:通过仅扫描一个软件包而不是整个项目,我们可以加快Spring Boot应用程序的启动速度。 Spring不会选择此包之外的任何带有Spring注释的类。
集成测试如下所示:
@RunWith(SpringJUnit4ClassRunner.class)
@WebIntegrationTest("server.port:0")
@SpringApplicationConfiguration(classes = DelayedWebServerConfiguration.class)
@ActiveProfiles("restTemplateTimeout")
public class RestTemplateShould {@Rulepublic ExpectedException thrown = none();@Value("${local.server.port}")private int port;@Autowiredprivate RestTemplate restTemplate;@Testpublic void throw_timeout_if_response_lasts_more_than_two_seconds() {thrown.expect(ResourceAccessException.class);thrown.expectCause(instanceOf(SocketTimeoutException.class));callEndpointWithDelay(3000);}@Testpublic void do_not_throw_timeout_if_response_lasts_less_than_two_seconds() {callEndpointWithDelay(10);}private void callEndpointWithDelay(long delayMs) {restTemplate.getForObject("http://localhost:" + port + "/test/delayRestTemplate?waitTimeMs=" + delayMs, String.class);}
}
当然,所有这些类都存储在我们的测试源文件夹(通常为src/test/java
)中,因为生产不需要它们。
让我们再看一下注解:
-
@RunWith
:测试将使用Spring Junit运行程序,该运行程序将负责为我们创建Spring上下文。 -
@WebIntegrationTest
:告诉Spring这是一个运行Web应用程序的集成测试,否则默认情况下Spring不会在测试模式下运行HTTP服务器。 我们还将server.port
的值设置为0
以便Spring Boot选择一个随机端口供HTTP服务器监听。 这允许并行运行多个测试,或者在后台运行该应用程序的另一个版本。 -
@SpringApplicationConfiguration
:我们告诉Spring在哪里可以找到我们之前创建的DelayedWebServerConfiguration
类。 -
@ActiveProfiles
:启用restTemplateTimeout
配置文件,否则Controller
和Configuration
将被过滤掉。
现在,我们有一个集成测试,它以一组有限的依赖关系而不是整个应用程序运行。 如果我们想走得更远并在游戏中添加模拟游戏怎么办? 当依赖项没有开发环境,或者过于复杂而无法从开发人员的工作站调用时,可能需要这样做。 在这种情况下,我们可以将这些模拟添加到Configuration
类中,并将它们注入到测试的Spring上下文中。
这是一个Configuration
示例,在该示例中,我们注入了一个由Mockito模拟的自定义CustomerService
,而不是默认值:
@Profile("validationTests")
@Configuration
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class})
@ComponentScan(basePackages = {"my.application.controller","my.application.actions"})
public class ValidationEndToEndConfiguration {@Beanpublic CustomerService customerService() {return Mockito.mock(CustomerService.class);}
}
通过这种方法,我们可以使集成测试更具弹性。 对于缓慢或不可靠的依赖关系,让开发人员针对模拟版本运行集成测试会更加有效。 但是,不要忘记,最终您的应用程序将必须与实际系统集成,而不是与模拟系统集成。 因此,让持续集成服务器至少每天都在真实系统上运行测试是有意义的。
翻译自: https://www.javacodegeeks.com/2016/02/isolating-integration-tests-mocking-dependencies-spring-boot.html