SpringBoot 单元测试:只加载需要的 Bean 以实现快速启动

3,310 阅读2分钟

问题背景

在 SpringBoot 中,如果使用 @SpringBootTest 注解进行单元测试的话,会启动很久

因为这种方式会加载所有的东西,我们希望在测试某个功能的时候,能快一点

解决的思路就是测试某个功能的时候,只加载这个功能需要的 Bean

最终实现的效果是这样的

@Slf4j
class XXXTest {

    private static TestService service;

    @BeforeAll
    static void beforeAll() {
        // 只加载需要的类,能节省大量的启动时间,加快单元测试
        ApplicationContext context = new AnnotationConfigApplicationContext(BeanConfig.class);
        service = context.getBean(TestService.class);
    }

    @Test
    void test01() {

        Test test = new Test();
        test.setName("sss");
        test.setCount(1);
        service.add(test);

    	List<Test> testList = service.findList();
        
        // 断言记录只有一条
        assertNotNull(testList);
        // 断言记录只有一条
        assertEquals(testList.size(), 1);
        // 断言数量和添加的数量一致
        assertEquals(0, test.getCount().compareTo(testList.get(0).getCount()));
    }
}

Bean 配置(BeanConfig.java)

@Configuration
@Import({XMLConfig.class})
public class BeanConfig {

    // Service 的 Bean 配置
    @Bean
    public TestService testService() {
        return new TestServiceImpl();
    }

    // Mapper 接口的配置(如果通过 MapperScannerConfigurer 扫描指定的路径(可以配置多个),也可以用这种方式)
    @Bean
    public MapperFactoryBean<TestMapper> TestMapper(SqlSessionFactory sqlSessionFactory) {
        MapperFactoryBean<TestMapper> mapperFactoryBean = new MapperFactoryBean<>();
        mapperFactoryBean.setMapperInterface(TestMapper.class);
        mapperFactoryBean.setSqlSessionFactory(sqlSessionFactory);
        return mapperFactoryBean;
    }

    // 以此类推,如果有依赖的 Bean,可以在这里使用 @Bean 注解初始化注入容器
}

Mapper XML 配置(XMLConfig.java)

import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.core.toolkit.GlobalConfigUtils;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import config.base.BaseDataSourceConfig;
import org.apache.ibatis.plugin.Interceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;
import java.io.IOException;

@Configuration
@Import({BaseDataSourceConfig.class})
public class XMLConfig {

    /**
     * 这里的 DataSource 会使用构造器注入的方式注入进来
     * 因为我们使用了 @Import 导入了 BaseDataSourceConfig.class
     * 所以 BaseDataSourceConfig 里面的 Bean 也会被注入容器
     * 
     * 注意:这里我使用的是 MyBatis-Plus 的插件,所以初始化的是 MybatisSqlSessionFactoryBean
	 */
    @Bean
    public MybatisSqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) throws IOException {
        // 限定符必须是表达式
        MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);

        // 设置 mapper.xml 位置
        ResourceLoader resourceLoader = new PathMatchingResourcePatternResolver();
        Resource TestMapper = resourceLoader.getResource("classpath:mapper/TestMapper.xml");
    	// 多个 Mapper 也同样用这种方式加载进来
        
        Resource[] resourceArray = new Resource[]{TestMapper};
        sessionFactory.setMapperLocations(resourceArray);

        // 设置插件
        Interceptor[] interceptors = new Interceptor[]{interceptor()};
        sessionFactory.setPlugins(interceptors);

        // 设置全局配置(不然无法使用 FillMetaObjectHandler)
        GlobalConfig globalConfig = GlobalConfigUtils.defaults();
        globalConfig.setMetaObjectHandler(new FillMetaObjectHandler()); // 注册该 Bean,不然单元测试无法设置 insert 字段
        sessionFactory.setGlobalConfig(globalConfig);

        return sessionFactory;
    }

    // 因为要加载不同包下面的 Mapper,为了启动快所以单独配置 Bean
    // 配置接口扫描的话会扫描所有的 Mapper
    /**
     * 配置 Mapper 接口扫描
     * @return
     */
    // @Bean
    // public MapperScannerConfigurer mapperScannerConfigurer() {
    //     MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
    //     mapperScannerConfigurer.setBasePackage("com.hytechpaas.workorder.modules.wos.mapper.stock");
    //     return mapperScannerConfigurer;
    // }

    
    // 这里的配置是我的项目中需要的配置,你需要根据自己项目的情况来决定是否需要这些配置
    /**
     * MyBatis 数据权限拦截器
     */
    @Bean
    public Interceptor interceptor() {
        return new com.common.datascope.DataScopeInterceptor();
    }

    /**
     * 数据权限开关配置
     */
    @Bean
    public DataScopeConfig dataScopeConfig() {
        return new DataScopeConfig();
    }

}

Mapper 需要数据源,所以初始化数据源(BaseDataSourceConfig.java)

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class BaseDataSourceConfig {

    @Bean
    public DataSource dataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/jeecg-boot?characterEncoding=UTF-8");
        dataSource.setUsername("root");
        dataSource.setPassword("1234");
        return dataSource;
    }

}

这种方式的优点和缺点

  1. 优点:减少容器启动时间,加快测试速度
  2. 缺点:当一个 Service 的依赖比较多的时候,配置 Bean 比较麻烦