如何进行`单元测试`

88 阅读2分钟
单元测试
  • 对最小单元(函数、方法、类)程序进行逻辑覆盖,并且获得正确的预期值
问题
  • 对于微服务/链路很长的服务如何处理
  1. 逐个方法/接口进行测试,方法内部依赖的其他方法/远程接口/IO等都进行mock,这些mock的东西单独再进行测试
  2. 逐个方法/接口进行测试,仅仅mock 远程接口,其他的(数据库IO/中间件【mq、redis、apollo等】)不mock 总结:暂不考虑严格唯独的单元测试,采用第二个方法
spring 环境下的单元测试
  • 中间件
  • maven
    • 插件
<build>
        <finalName>upex-act-promotion-job</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <fork>true</fork>
                    <addResources>true</addResources>
                </configuration>
            </plugin>
            <!-- 测试插件 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.1</version>
                <configuration>
                    <skipTests>false</skipTests>
                </configuration>
            </plugin>
        </plugins>
    </build>
    • 内存 apollo
<dependency>
  <groupId>com.ctrip.framework.apollo</groupId>
  <artifactId>apollo-mockserver</artifactId>
  <version>2.0.0</version>
  <scope>test</scope>
</dependency>

    • 内存数据库
<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <scope>test</scope>
</dependency>
    • redis
<dependency>
  <groupId>it.ozimov</groupId>
  <artifactId>embedded-redis</artifactId>
  <version>0.7.2</version>
  <scope>test</scope>
</dependency>
    • okhttp3 controller测试
<dependency>
  <groupId>com.squareup.okhttp3</groupId>
  <artifactId>okhttp</artifactId>
  <version>3.11.0</version>
  <scope>test</scope>
</dependency>
    • spring 测试环境
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>
    • mockkio
<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-inline</artifactId>
  <version>3.11.2</version>
  <scope>test</scope>
</dependency>
  • 配置
    • apollo 配置类
@Configuration
@EnableApolloConfig("application")
public class ApolloConfig {
    @ClassRule
    public static EmbeddedApollo embeddedApollo = new EmbeddedApollo();
}
自动加载classPath下的mockdata-application.properties
    • H2数据库配置
@Configuration
@EnableApolloConfig("application")
@Slf4j
public class H2Config {

    @Primary
    @Bean("dataSource")
    public DataSource dataSource() {
        Config appConfig = ConfigService.getAppConfig();
        DataSource ds = DataSourceBuilder.create()
                .driverClassName(appConfig.getProperty("spring.datasource.upex.driverClassName","org.h2.Driver"))
                .url(appConfig.getProperty("spring.datasource.upex.url","jdbc:h2:mem:upex_act;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false;"))
                .username(appConfig.getProperty("spring.datasource.upex.username","pdai"))
                .password(appConfig.getProperty("spring.datasource.upex.password","pdai"))
                .build();
        // 创建 ResourceDatabasePopulator
        String property = appConfig.getProperty("unit.sql", "init.sql");
        String[] sqls = property.split(",");
        ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator();
        for (String sql:sqls){
            databasePopulator.addScript(new ClassPathResource(sql));
        }
        // 数据源初始化
        DataSourceInitializer dataSourceInitializer = new DataSourceInitializer();
        dataSourceInitializer.setDataSource(ds);
        dataSourceInitializer.setDatabasePopulator(databasePopulator);
        dataSourceInitializer.setEnabled(true);
        dataSourceInitializer.afterPropertiesSet();
        return ds;
    }

    @Bean
    @Primary
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") final DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();

        // 配置MyBatis的XML映射文件路径
        Resource[] resources = resolver.getResources("classpath:/mybatis/mapper/*.xml");
        sessionFactory.setMapperLocations(resources);
        SqlSessionFactory sqlSessionFactory = sessionFactory.getObject();
        assert sqlSessionFactory != null;
        org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();
        configuration.setMapUnderscoreToCamelCase(true);
        return sqlSessionFactory;

    }

    @Bean
    @Primary
    public PlatformTransactionManager transactionManager(@Qualifier("dataSource") final DataSource dataSource){
        return new DataSourceTransactionManager(dataSource);
    }

    @PreDestroy
    public void preDestroy(){
        try (Connection connection = dataSource().getConnection(); Statement dropTableStatement = connection.createStatement()) {
            String schema = connection.getSchema();
            DatabaseMetaData metaData = connection.getMetaData();
            // 3. 获取表信息
            ResultSet tables = metaData.getTables(null, schema, null, new String[]{"TABLE"});
            while (tables.next()) {
                String tableName = tables.getString("TABLE_NAME");
                String dropTableSql = "drop table " + tableName + " cascade";
                dropTableStatement.execute(dropTableSql);
            }
            connection.commit();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}
初始化数据库
    • redis配置
@Configuration
@Slf4j
public class RedisConfig{
    private RedisServer redisServer;
    @PostConstruct
    public void postConstruct() {
        log.info("redis内存测试数据库starting......");
        redisServer = RedisServer.builder()
                .port(6379)
                .setting("maxmemory 128M") //maxheap 128M
                .build();
        redisServer.start();
        log.info("redis内存测试数据库started !!!!!!");
    }

    @PreDestroy
    public void preDestroy() {
        redisServer.stop();
        log.info("redis内存测试数据库stop !!!!!!");
    }
}
    • 测试基类
**
 * 基础测试类
 * @author fly.cheng
 **/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringJUnitConfig(classes = {
        ApolloConfig.class,
        RedisConfig.class,
        H2Config.class,
        TransactionHelper.class,
        RedissonAutoConfig.class,
        ActivityRedisService.class,
        ApTransactionServiceImpl.class
})
@EnableApolloConfig("application")
@MapperScan(basePackages = "com.xxx.service.**.mapper")
@WebAppConfiguration
public abstract class BaseTest {
    @ClassRule
    public static EmbeddedApollo embeddedApollo = new EmbeddedApollo();
    @Resource
    RedisConfig redisConfig;
    @Resource
    H2Config h2Config;

    @After
    public void clear(){
        redisConfig.preDestroy();
        h2Config.preDestroy();
    }
}
1、扫描持久层
2、注入 redis、h2、apollo bean
3、清理资源
注解
  • @MockBean spring boot starter test
  • mockStatic(xxx.class) mockito
  • when(xx.xx())thenReturn() mockito
  • @Test junit
  • @DisplayName junit
  • @RunWith(SpringJUnit4ClassRunner.class) junit
  • @SpringJUnitConfig(classes = { xxx.class }) spring junit
  • Assert.assertXxxxxx junit