使用SpringBoot进行软件单元测试

发表于:2020-11-09 09:32

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:袖子    来源:博客园

  SpringBoot提供了许多实用程序和注释,可以在测试应用程序时提供帮助。测试支持由两个模块提供:spring-boot-test包含核心项,spring-boot-test-autoconfigure支持测试的自动配置。
  大多数开发人员使用spring-boot-starter-test启动器,它既导入SpringBoot测试模块,也导入了JUnit Jupiter, AssertJ,  Hamcrest和其他一些有用的库。
  starter还带来了老式引擎,因此你可以同时运行JUnit 4和JUnit 5测试。如果你已将测试迁移到JUnit 5,则应排除对JUnit 4的支持,如下例所示:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
    </exclusions>
</dependency>
  一、测试范围依赖项
  spring-boot-starter-test在test域包含以下提供的库:
  JUnit5(包括与JUnit4向后兼容的vintage引擎):Java应用程序单元测试的事实标准。
  Spring Test&Spring Boot Test:对springboot应用程序的实用程序和集成测试支持。
  AssertJ:一个流畅的断言库。
  Hamcrest:matcher对象库(也称为约束或谓词)。
  Mockito:一个Java模拟框架。
  JSONassert:JSON的断言库。
  JsonPath:JSON的XPath。
  我们通常发现这些公共库在编写测试时非常有用。如果这些库不适合你的需要,你可以添加自己的附加测试依赖项。
  二、测试Spring应用
  依赖注入的一个主要优点是它可以使代码更容易进行单元测试。你可以使用new操作符实例化对象,甚至不需要涉及Spring。你还可以使用模拟对象而不是实际的依赖项。
  通常,你需要超越单元测试并开始集成测试(使用Spring ApplicationContext)。能够在不需要部署应用程序或连接到其他基础设施的情况下执行集成测试非常有用。
  Spring框架包括一个专用的测试模块,用于这种集成测试。可以直接向声明依赖项org.springframework:spring-test或使用spring-boot-starter-test来过渡地将其拉入。
  三、测试SpringBoot应用
  SpringBoot应用程序是Spring ApplicationContext,因此除了常规的Spring上下文之外,不需要做任何特别的测试。
  默认情况下,只有使用SpringApplication创建springboot的外部属性、日志记录和其他特性才会安装在上下文中。
  SpringBoot提供了一个@SpringBootTest注解,当你需要springboot特性时,它可以作为标准spring-test @ContextConfiguration的替代品。注解的工作原理是通过SpringApplication创建测试中使用的ApplicationContext。除了@SpringBootTest之外,还提供了许多其他注解来测试应用程序的更具体的部分。
  如果你使用的是JUnit 4,别忘了添加@RunWith(SpringRunner.class)否则注释将被忽略。如果你使用的是JUnit 5,则不需要添加等效的@ExtendWith(SpringExtension.class),注解@SpringBootTest和其他@…测试注释已经用它注释了。
  默认情况下,@SpringBootTest不会启动服务器。你可以使用@SpringBootTest的webEnvironment属性进一步优化测试的运行方式:
  MOCK(默认):加载web应用程序上下文并提供模拟web环境。使用此注解时不会启动嵌入式服务器。如果类路径上没有可用的web环境,则此模式透明地返回到创建常规的非web应用程序上下文。它可以与@AutoConfigureMockMvc或@AutoConfigureWebTestClient结合使用,以对web应用程序进行基于模拟的测试。
  RANDOM_PORT:加载一个WebServerApplicationContext并提供一个真实的web环境。嵌入式服务器启动并在随机端口上侦听。
  DEFINED_PORT:加载WebServerApplicationContext并提供真实的web环境。嵌入式服务器启动并在定义的端口上侦听(从application.properties)或者在默认端口8080上。
  NONE:使用SpringApplication加载ApplicationContext,但不提供任何web环境(mock或其他)。
  如果你的测试是@Transactional,那么默认情况下,它会在每个测试方法的末尾回滚事务。但是,由于使用RANDOM_PORT或DEFINED_PORT的这种安排隐式地提供了一个真正的servlet环境,HTTP客户机和服务器在不同的线程中运行,因此,在单独的事务中运行。在这种情况下,在服务器上启动的任何事务都不会回滚。
  @SpringBootTest中webEnvironment = WebEnvironment.RANDOM_PORT将会使用不同的端口启动管理服务器
  检测Web应用程序类型
  如果Spring MVC可用,那么将配置一个常规的基于MVC的应用程序上下文。如果你只有Spring WebFlux,我们将检测到它并配置一个基于WebFlux的应用程序上下文。
  如果两者都存在,则Spring MVC优先。如果要在此场景中测试反应式web应用程序,则必须设置spring.main.web-application-type属性:
@SpringBootTest(properties = "spring.main.web-application-type=reactive")
class MyWebFluxTests { ... }
  检测测试配置
  如果你熟悉Spring测试框架,你可能习惯于使用@ContextConfiguration(classes=…)来指定要加载哪个Spring@Configuration。或者,你可能经常在测试中使用嵌套的@Configuration类。
  在测试springboot应用程序时,通常不需要这样做。当你没有显式定义主配置时,springboot的@*Test注解会自动搜索主配置。
  搜索算法从包含测试的包开始工作,直到找到一个用@SpringBootApplication或@SpringBootConfiguration注释的类。只要你以一种合理的方式组织代码,你的主要配置就会被找到。
  如果要自定义主配置,可以使用嵌套的@TestConfiguration类。与嵌套的@Configuration类不同,它将代替应用程序的主配置,除了应用程序的主配置之外,还使用了嵌套的@TestConfiguration类。
  Spring的测试框架在测试之间缓存应用程序上下文。因此,只要测试共享相同的配置(无论如何发现),加载上下文的潜在耗时过程只会发生一次。
  排除测试配置
  如果你的应用程序使用组件扫描(例如,如果你使用@SpringBootApplication或@ComponentScan),你可能会发现只为特定测试创建的顶级配置类在任何地方都会被意外地找到。
  如前所述,@TestConfiguration可用于测试的内部类,以自定义主配置。当放在顶级类上时,@TestConfiguration表示src/test/java中的类不应该通过扫描获取。然后,可以在需要的地方显式导入该类,如下例所示:
@SpringBootTest
@Import(MyTestsConfiguration.class)
class MyTests {

    @Test
    void exampleTest() {
        ...
    }

}
  如果你直接使用@ComponentScan(也就是说,不是通过@SpringBootApplication),你需要用它注册TypeExcludeFilter。
  使用应用程序参数
  如果您的应用程序需要参数,您可以让@SpringBootTest使用args属性注入它们。
@SpringBootTest(args = "--app.test=one")
class ApplicationArgumentsExampleTests {

    @Test
    void applicationArgumentsPopulated(@Autowired ApplicationArguments args) {
        assertThat(args.getOptionNames()).containsOnly("app.test");
        assertThat(args.getOptionValues("app.test")).containsOnly("one");
    }

}
  使用模拟环境进行测试
  默认情况下,@SpringBootTest不启动服务器。如果您有要在此模拟环境下测试的web端点,则可以另外配置MockMvc,如下例所示:
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
class MockMvcExampleTests {

    @Test
    void exampleTest(@Autowired MockMvc mvc) throws Exception {
        mvc.perform(get("/")).andExpect(status().isOk()).andExpect(content().string("Hello World"));
    }

}
  如果您只想关注web层而不想启动完整的ApplicationContext,可以考虑改用@WebMvcTest。或者,可以配置WebTestClient,如下例所示:
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.reactive.server.WebTestClient;

@SpringBootTest
@AutoConfigureWebTestClient
class MockWebTestClientExampleTests {

    @Test
    void exampleTest(@Autowired WebTestClient webClient) {
        webClient.get().uri("/").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("Hello World");
    }

}
  在模拟环境中进行测试通常比使用完整的Servlet容器运行要快。但是,由于mocking发生在Spring MVC层,依赖于较低级别Servlet容器行为的代码不能用MockMvc直接测试。
  例如,SpringBoot的错误处理基于Servlet容器提供的“error page”支持。这意味着,虽然可以测试MVC层抛出和处理异常,但不能直接测试是否呈现了特定的自定义错误页。如果需要测试这些较低级别的关注点,可以启动一个完全运行的服务器。
  使用正在运行的服务器进行测试
  如果您需要启动一个完全运行的服务器,我们建议您使用随机端口。如果使用@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT),则每次运行测试时随机选择一个可用端口。
  @LocalServerPort注解可用于将实际使用的端口注入到测试中。为了方便起见,需要对启动的服务器进行REST调用的测试可以另外@Autowire一个WebTestClient,它解析到正在运行的服务器的相关链接,并附带一个用于验证响应的专用API,如下例所示:如下一节所述。
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.test.web.reactive.server.WebTestClient;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class RandomPortWebTestClientExampleTests {

    @Test
    void exampleTest(@Autowired WebTestClient webClient) {
        webClient.get().uri("/").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("Hello World");
    }

}
  这个设置需要类路径上的spring-webflux。如果您不能或不想添加webflux,Spring Boot还提供了一个TestRestTemplate工具:
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class RandomPortTestRestTemplateExampleTests {

    @Test
    void exampleTest(@Autowired TestRestTemplate restTemplate) {
        String body = restTemplate.getForObject("/", String.class);
        assertThat(body).isEqualTo("Hello World");
    }

}
  自定义WebTestClient
  要定制WebTestClient bean,请配置一个WebTestClientBuilderCustomizer bean。任何这样的bean都是使用WebTestClient.Builder来创建WebTestClient。
  使用JMX
  由于测试上下文框架缓存上下文,默认情况下禁用JMX,以防止相同的组件在同一个域上注册。如果此类测试需要访问MBeanServer,请考虑将其标记为“脏”:
@ExtendWith(SpringExtension.class)
@SpringBootTest(properties = "spring.jmx.enabled=true")
@DirtiesContext
class SampleJmxTests {

    @Autowired
    private MBeanServer mBeanServer;

    @Test
    void exampleTest() {
        // ...
    }

}
  Mocking 和 Spying Beans
  运行测试时,有时需要在应用程序上下文中模拟某些组件。例如,您可能在开发过程中无法使用的远程服务上有一个facade。当您想要模拟在真实环境中很难触发的故障时,模拟也很有用。
  SpringBoot包含一个@MockBean注释,可用于为ApplicationContext中的bean定义Mockito mock。您可以使用注释添加新的bean或替换现有的单个bean定义。注释可以直接用于测试类、测试中的字段或@Configuration类和字段。在字段上使用时,创建的mock的实例也会被注入。模拟bean在每个测试方法之后都会自动重置。
  如果您的测试使用springboot的测试注释之一(比如@SpringBootTest),这个特性会自动启用。要以不同的方式使用此功能,必须显式添加侦听器,如下例所示:
@TestExecutionListeners(MockitoTestExecutionListener.class)
  以下示例使用模拟实现替换现有的RemoteService bean:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.*;
import org.springframework.boot.test.context.*;
import org.springframework.boot.test.mock.mockito.*;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.BDDMockito.*;

@SpringBootTest
class MyTests {

    @MockBean
    private RemoteService remoteService;

    @Autowired
    private Reverser reverser;

    @Test
    void exampleTest() {
        // RemoteService has been injected into the reverser bean
        given(this.remoteService.someCall()).willReturn("mock");
        String reverse = reverser.reverseSomeCall();
        assertThat(reverse).isEqualTo("kcom");
    }

}
  @MockBean不能用于模拟在应用程序上下文刷新期间执行的bean的行为。在执行测试时,应用程序上下文刷新已经完成,配置模拟行为已经太晚了。我们建议在这种情况下使用@Bean方法来创建和配置mock。

  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号