SpringBoot 测试小记,优化测试用例的启动速度

本文概述

  1. 在SpringBoot 中测试用例简单演示
  2. 在测试用例中通过指明扫描范围,加快spring容器的启动速度
  3. 通过使用h2内存数据库加快测试速度和隔离测试环境和他环境数据相互影响的问题

Spring扫描范围和启动速度

随着业务的发展,项目复杂度增加引用的jar和业务代码越来越多,Spring应用在启动时需要扫描和实例化装载的Bean越来越多,以及环境上下文(配置加载,初始化第三方组件)的处理,这势必会导致启动时间边长,特别是有中间件(第三方服务)的依赖的时候(例如连接数据库、消息队列、NoSql等)。

但是在测试的时候,我们可能只是测试单个方法或者是一组业务方法,需要的环境和Bean对象只是几个或者十几个,一般来说都远远小于整个项目启动说需要的Bean数量,所以我们在写测试的时候可以考虑通过指定Spring的在启动是需要初始化的Bean,以及启动Web容器(例如Tomcat)来加快Spring容器启动的速度。这样在我们反复的跑测试代码的时候可以节约不少时间,提高效率。

举一些栗子`

控制器测试栗子:

@DisplayName("测试MemberController")
@SpringBootTest(classes = {SpringBootTestExampleApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class MemberControllerTest {

    @Autowired
    private WebApplicationContext webApplicationContext;

    private MockMvc mockMvc;

    @BeforeEach
    public void setUp() throws Exception {
        //MockMvcBuilders.webAppContextSetup(WebApplicationContext context):指定WebApplicationContext,将会从该上下文获取相应的控制器并得到相应的MockMvc;
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();//建议使用这种
    }

    @DisplayName("测试MemberController#findById方法")
    @Test
    public void testFindById() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/member/1")
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();
    }
}

服务层测试栗子

@DataJpaTest(properties = {"spring.jpa.hibernate.auto-ddl=update"},
        excludeAutoConfiguration = {JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class})
@ActiveProfiles("test")
@ComponentScan(value = {"com.wuxp.study.services", "com.wuxp.study.repositories"})
class MemberServiceImplTest {

    @Autowired
    private MemberService memberService;

    @Test
    public void testFindById() {
        Member member = memberService.findById(1L);
        Assertions.assertNull(member);
    }
}

Spring Mockito 测试栗子

@SpringBootTest(classes = {MockitoServiceMockTest.MockitoConfiguration.class}, webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class MockitoServiceMockTest {

    @MockBean
    private MemberService memberService;

    @Autowired
    private MockitoService mockitoService;

    @Test
    public void testFindById() {
        Member result = new Member();
        result.setId(1L);
        given(this.memberService.findById(anyLong())).willReturn(result);
        Member member = mockitoService.findById(1L);
        Assertions.assertEquals(result, member);
    }

    @Configuration
    // 指定需要初始化的Bean
    @Import({MockitoService.class, MemberService.class})
    static class MockitoConfiguration {
    }
}


@DataJpaTest
public class MockitoServiceSpyTest {

    /**
     * 来表示一个“间谍对象”,允许它的某些方法被模拟,而剩下的方法仍然是真实的方法。
     */
    @SpyBean
    private MemberServiceImpl memberService;


    @Test
    public void testFindById() {
        Member result = new Member();
        result.setId(1L);
        given(this.memberService.findById(anyLong())).willReturn(result);
        Member member = memberService.findById(1L);
        Assertions.assertEquals(result, member);
        Assertions.assertEquals(memberService.findNameById(1L), "name@1");
    }
    
}

上面的例子简单的列举了SpringBoot中一些测试的用法,主要演示了

@DataJpaTest(properties = {"spring.jpa.hibernate.auto-ddl=update"},
        excludeAutoConfiguration = {JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class})

@SpringBootTest(classes = {MockitoServiceMockTest.MockitoConfiguration.class}, webEnvironment = SpringBootTest.WebEnvironment.NONE)

@ComponentScan(value = {"com.wuxp.study.services","com.wuxp.study.repositories"})

这几个注解在测试中的用法

  1. DataJpaTest:只关注JPA组件的JPA测试的注解。使用此注解将禁用完全自动配置。就是说这个注解只会启动部分的自动配置,而不是所有的自动配置(SprinBoot有许多的自动配置,或者是自定义的自动配置)。
  2. SpringBootTest:标记一个类为SpringBoot的测试入口类,classes属性用于指定Spring容器在启动是依赖的配置类,可以用于缩小需要初始化的Bean范围,从而提高测试效率。
  3. ComponentScan:这个组件是用来指明Spring 扫描Bean的范围的。

在真实的项目中,在写测试的,由于组件之间的依赖比较复杂,依靠SpringBootTest注解的classes和DataJpaTest注解的excludeAutoConfiguration(只能排除自动配置类型的配置类)属性做配置排除和Bean扫描(初始化范围指定的时候)配置无法满足测试的需求的使用,可以使用ComponentScan注解做更为灵活的配置

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
    /**
     * 对应的包扫描路径 可以是单个路径,也可以是扫描的路径数组
     * @return
     */
    @AliasFor("basePackages")
    String[] value() default {};
    /**
     * 和value一样是对应的包扫描路径 可以是单个路径,也可以是扫描的路径数组
     * @return
     */
    @AliasFor("value")
    String[] basePackages() default {};
    /**
     * 用于指定要扫描带注释组件的包的basePackages的类型安全替代方法。将扫描指定的每个类的包.
     * 考虑在每个包中创建一个特殊的no-op标记类或接口,该类或接口除了被该属性引用之外没有其他用
     * 途。
     * 
     *   basePackageClasses={A.class} 等价于 
     *   basePackages={A.Class..getPackage().getName()}
     * 
     * @return
     */
    Class<?>[] basePackageClasses() default {};
    /**
     * 对应的bean名称的生成器 默认的是BeanNameGenerator
     * @return
     */
    Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
    /**
     * 处理检测到的bean的scope范围
     */
    Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;
    /**
     * 是否为检测到的组件生成代理
     * Indicates whether proxies should be generated for detected components, which may be
     * necessary when using scopes in a proxy-style fashion.
     * 

The default is defer to the default behavior of the component scanner used to * execute the actual scan. *

Note that setting this attribute overrides any value set for {@link #scopeResolver}. * @see ClassPathBeanDefinitionScanner#setScopedProxyMode(ScopedProxyMode) */ ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT; /** * 控制符合组件检测条件的类文件 默认是包扫描下的 **/*.class * @return */ String resourcePattern() default ClassPathScanningCandidateComponentProvider.DEFAULT_RESOURCE_PATTERN; /** * 是否对带有@Component @Repository @Service @Controller注解的类开启检测,默认是开启的 * @return */ boolean useDefaultFilters() default true; /** * 指定某些定义Filter满足条件的组件 FilterType有5种类型如: * ANNOTATION, 注解类型 默认 ASSIGNABLE_TYPE,指定固定类 ASPECTJ, ASPECTJ类型 REGEX,正则表达式 CUSTOM,自定义类型 * @return */ Filter[] includeFilters() default {}; /** * 排除某些过来器扫描到的类 * @return */ Filter[] excludeFilters() default {}; /** * 扫描到的类是都开启懒加载 ,默认是不开启的 * @return */ boolean lazyInit() default false; }

使用方式请参考
Spring注解——使用@ComponentScan自动扫描组件

@ContextConfiguration

定义类级元数据,用于确定如何为集成测试加载和配置,简单的说就是通过该注解定义Spring容器在启动是需要加载的上下文(配置类、配置文件、实例化的Bean),可以用于最小化测试,提升测试效率。

使用H2作为测试环境的数据库

一般来说我们生产和开发环境的数据库是分开的,但是测试的时候是不是可以考虑和开发环境用同一个呢?

最好是不要,由于测试的时候产生的数据库操作,可能会造成脏数据,影响开发、或者是开发环境造的数据会影响测试,所以更建议使用h2数据库的内存模式作为测试是使用,它有以下优点:

  1. 启动快,这个很重要,可以减少我们用例跑的时长
  2. 每一次都是一个新的测试环境,没有历史脏数据的烦恼

在测试的时候使用,只需要引入依赖就好

  <dependency>
      <groupId>com.h2databasegroupId>
      <artifactId>h2artifactId>
      <scope>testscope>
    dependency>

如果你的测试没有使用DataJpaTest,只需要在测试目录下的加一个appliaction-test.yaml的配置文件,配置上数据源信息就好

spring:
  datasource:
    username: sa
    password: sa
    url: jdbc:h2:mem:dbtest;MODE=MySQL
    platform: h2
    driver-class-name: org.h2.Driver

测试数据的来源和Mock

通过数据源的配置

spring:
  datasource:
    ### 建表语言
    schema:
      - classpath:sql/jdbc-schema.sql
    ### 初始化表数据的语句 
    data: 
      - classpath: jdbc-data.sql

或者是在注解上指定

@DataJpaTest(properties = {"spring.datasource.schema=classpath:jdbc-schema.sql"}@SpringBootTest(properties = {"spring.datasource.schema=classpath:jdbc-schema.sql"}

spring test在org.springframework.test.context.jdbc下面提供了一组sql相关的组件,用于在测试方法执行的前后,执行一些Sql语句,用于初始化数据或者清除数据。

举个栗子

@Sql(scripts = "/insert_user.sql", statements = "insert into t_user(id, name) values (100, '张三')")

参考文章
Spring 单元测试中使用@Sql准备数据

额外补充:在测试用例上加上@Transactional注解,测试执行后无论测试方式是否抛出异常,事务都会被回滚。

Spring 5 启动性能优化之 @Indexed

代码git地址

你可能感兴趣的:(Spring杂谈,SpringBoot,Test,单元测试,spring,boot)