美文网首页
提高单元测试能力

提高单元测试能力

作者: 那就省略号吧 | 来源:发表于2021-10-11 16:45 被阅读0次

    单元测试痛点

    1.开发人员在编写单元测试时,不免会与数据库进行数据交互,有查询,新增,删除,修改等操作,除查询之外的操作都会造成数据库脏数据的产生,会影响后续的测试,经年累月之后,不得不重新导入一份完整的数据以继续供测试时使用;

    2.另外在开发分布式项目,也会经常与远程服务进行交互,当远程服务宕机时,则会影响测试进度;如果A服务功能需要依赖B服务,两个服务都处于开发中,如果A提前开发完,进行测试时,由于B服务还处于开发中则无法调用B服务;当远程服务都正常运行,在服务之间调用时,又会不免产生大量的脏数据,如果想处理脏数据,又需要去了解远程服务的业务,以及表结构,大大影响效率;

    3.测试分支过多,需要编写多种测试用例,工作繁琐,往往工作量是编写接口的好几倍。

    解决方式

    业务代码

    省了mapper层和controller层及一些实体类代码,有需要可以到代码参考下载项目进行参考

    public interface CategoryService {
        void deleteById(Long id);
    
        Category findById(Long id);
    
        Long save(Category category);
    
        String getTypeDesc(Integer type);
    
    }
    
    @Service
    @AllArgsConstructor
    public class CategoryServiceImpl implements CategoryService {
        private final CategoryDao categoryDao;
        private final RemoteRpc remoteRpc;
    
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void deleteById(Long id) {
            categoryDao.deleteById(id);
        }
    
        @Override
        public Category findById(Long id) {
            return categoryDao.findById(id);
        }
    
        @Override
        public Long save(Category category) {
            categoryDao.save(category);
            return category.getId();
        }
    
        @Override
        public String getTypeDesc(Integer type) {
            if (type==1){
                return "ONE";
            }else if (type==2){
                return "TWO";
            }else if (type==3){
                return "THREE";
            }else {
                return "OTHER";
            }
        }
    }
    
    public interface SnacksService {
        String delete(Long id);
    }
    
    @Service
    @Slf4j
    @AllArgsConstructor
    public class SnacksServiceImpl implements SnacksService {
        private final RemoteRpc remoteRpc;
    
        @Override
        public String delete(Long id) {
            log.info("删除成功,开始调用远程服务");
            return remoteRpc.invork(id.toString());
        }
    }
    
    @Component
    @Slf4j
    public class RemoteRpc {
        public String invork(String param){
            log.info("远程服务调用:{}",param);
            return "SUCCESS";
        }
    }
    
    
    内存数据库

    不产生脏数据的方式就是在根源上杜绝与数据库产生交互,使用内存数据库是一个比较有效的途径,这边采用的是H2数据库,在单元测试启动时,会根据我们指定的建表语句和需要插入数据的sql文件来创建内存数据库,之后的数据交互遍便是与内存数据库进行。

    • 项目结构
    image
    • pom
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.4.3.2</version>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>test</scope>
    </dependency>
    
    
    • yml
    spring:
      datasource:
        url: jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=MySQL
        username:
        password:
        driver-class-name: org.h2.Driver
    #    指定数据源
        data: classpath:data.sql
    #    指定需要建表语句
        schema: classpath:schema.sql
    
    mybatis-plus:
      configuration:
    #    控制台打印sql 
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    
    
    • 单元测试基类
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.ActiveProfiles;
    
    //指定启动时加载的配置文件
    @ActiveProfiles("test")
    @SpringBootTest
    public class BaseTest {
    }
    
    
    • 业务测试类
    import com.pdl.memory_database.domain.Category;
    import com.pdl.memory_database.service.CategoryService;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    
    import java.util.Date;
    
    public class CategoryTest extends BaseTest {
        @Autowired
        private CategoryService categoryService;
    
        /*
        实际删除的为内存数据库的数据,并不会删除原数据库的数据
        */
        @Test
        void delete() {
            categoryService.deleteById(1L);
        }
    }
    
    
    使用junit进行多分支测试

    当我们编写的接口需要验证多种情况下的返回结果,可以使用junit框架内部的两个注解:@ParameterizedTest和@CsvSource,通过注解的方法,构造不同情况下的入参,以及不同入参下返回的期望结果值。其中注解@CsvSource中的value值会映射到定义好的入参中。如果入参为对象时,则需要通过实现ArgumentsAggregator类下的方法aggregateArguments,指定@CsvSource中的value转为对应的对象,并在方法入参中进行指定

    • @CsvSource参数转换为对象
    import com.pdl.memory_database.domain.Category;
    import org.junit.jupiter.api.extension.ParameterContext;
    import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
    import org.junit.jupiter.params.aggregator.ArgumentsAggregationException;
    import org.junit.jupiter.params.aggregator.ArgumentsAggregator;
    
    public class CategoryArguments implements ArgumentsAggregator {
        @Override
        public Object aggregateArguments(ArgumentsAccessor argumentsAccessor, ParameterContext parameterContext)
            throws ArgumentsAggregationException {
            Category category = new Category();
            category.setName(argumentsAccessor.getString(1));
            category.setStatus(argumentsAccessor.getInteger(2));
            category.setIsDelete(argumentsAccessor.getInteger(3));
            return category;
        }
    }
    
    
    • 单元测试
    public class MultiBranchTest extends BaseTest {
        @Autowired
        private CategoryService categoryService;
    
        @ParameterizedTest
        @CsvSource(value = {
        "1,ONE", //场景1
        "2,TWO", //场景2
        "3,THREE", //场景3
        "4,OTHER" //场景4
        })
        void getTypeDesc(Integer type, String expectation) {
            String typeDesc = categoryService.getTypeDesc(type);
            Assertions.assertEquals(expectation, typeDesc);
        }
    
        @ParameterizedTest
        @CsvSource(value = {
            "2,蒙牛,1,0", //场景1
            "3,伊利,1,0" //场景2
        })
        void saveCategory(Long expectId, @AggregateWith(CategoryArguments.class) Category category) {
            Long id = categoryService.save(category);
            Assertions.assertEquals(id, expectId);
        }
    }
    
    
    使用spock框架进行多分支测试

    Spockk是一个Java和Groovy应用的测试和规范框架,基于BDD(行为驱动开发)思想实现,功能非常强大。Spock结合Groovy动态语言的特点,提供了各种标签,并采用简单、通用、结构化的描述语言,让编写测试代码更加简洁、高效。以下为spock的标签及其作用

    • given:输入条件(前置参数)。

    • when:执行行为(Mock接口、真实调用)。

    • and:衔接上个标签,补充的作用。

    • then:输出条件(验证结果)。

    • with:配合then进行使用,对结果值进行校验。

    • where:通过表格的方式来测试多种分支。

      • pom
    
    <dependency>
        <groupId>org.spockframework</groupId>
        <artifactId>spock-core</artifactId>
        <version>1.3-groovy-2.4</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.spockframework</groupId>
        <artifactId>spock-spring</artifactId>
        <version>1.3-RC1-groovy-2.4</version>
        <scope>test</scope>
    </dependency>groovy
    <dependency>
        <groupId>org.codehaus.groovy</groupId>
        <artifactId>groovy-all</artifactId>
        <version>2.4.6</version>
    </dependency>
    
    

    单元测试

    import com.pdl.memory_database.domain.Category
    import com.pdl.memory_database.rpc.RemoteRpc
    import com.pdl.memory_database.service.CategoryService
    import com.pdl.memory_database.service.SnacksService
    import org.mockito.Mockito
    import org.springframework.beans.factory.annotation.Autowired
    import org.springframework.boot.test.context.SpringBootTest
    import org.springframework.boot.test.mock.mockito.MockBean
    import org.springframework.test.context.ActiveProfiles
    import spock.lang.Specification
    import spock.lang.Unroll
    
    /*
    使用spock进行单元测试
     */
    @ActiveProfiles("test")
    @SpringBootTest
    class SpockControllerTest extends Specification {
        @Autowired
        private CategoryService categoryService;
        @MockBean
        private RemoteRpc remoteRpc;
        @Autowired
        private SnacksService snacksService;
    
        @Unroll
        def "查询数据"() {
            when: "查询信息"
            def category = categoryService.findById(1)
    
            then: "结果"
            with(category) {
                id == 1
                name == "油炸食品"
            }
        }
    
        /*
        多分支测试时,返回结果比较单一时,直接用expect校验准确性
         */
        @Unroll
        def "多分支测试,参数:#type,期望:#typeDesc"() {
            expect:
            categoryService.getTypeDesc(type) == typeDesc
    
            where: "测试不同分支"
            type || typeDesc
            1    || "ONE"
            2    || "TWO"
            3    || "THREE"
        }
    
        /*
         多分支测试时,返回结果为对象,可以通过then比较对象内部值
          */
        @Unroll
        def "保存数据: #category,结果:#id"() {
            when: "保存数据"
            def categoryId = categoryService.save(category)
    
            then: "结果验证"
            with(categoryId) {
                categoryId == id
            }
    
            where: "参数"
            category                                                 || id
            new Category(null, "蒙牛酸奶", 1, 0, new Date(), new Date()) || 2
            new Category(null, "伊利酸奶", 1, 0, new Date(), new Date()) || 3
        }
    
        /*
        多分支测试时,需要mock时
         */
        @Unroll
        def "结合mock进行多分支测试,参数:#id,结果:#rpcResult"() {
            when:"调用远程"
            Mockito.when(remoteRpc.invork(id.toString())).thenReturn(rpcResult)
            def result = snacksService.delete(id)
    
            then: "结果验证"
            with(result) {
                result == rpcResult
            }
            where: "参数"
            id || rpcResult
            1  || "SUCCESS"
            2  || "FAILED"
        }
    }
    
    
    Junit和spock进行单元测试对比
    junit spock
    语法 通过Java语言进行编写 创建的测试类为Groovy class,开发语言与Java有些许不同,有特定的语法和格式,有部分写法与java完全相同,如方法调用,mock调用等,简单易学易上手
    代码可读性 可读性依赖于代码编写的好坏 可通过中文对方法进行定义解释,执行步骤一目了然,可读性很高
    多分支测试 当入参为对象时,需要先实现ArgumentsAggregator类定义参数转化的类,如果@CsvSource中的value内参数顺序有进行调换时,只需要修改参数转化的方法,且一个对象对应一个转化类,较为繁琐,不容易维护 可以直接通过where标签里定义不同的参数,直接new出来,无需创建转换类,更加方便,易于管理
    解决远程服务调用

    Mockito是mocking框架,它让你用简洁的API做测试。通过调用提供的API在执行对应方法前定义我们期望的结果,当执行到需要mock的方法时,则会返回我们需要的结果值,而不用去调用内部的业务逻辑。它不仅适用于远程服务调用,也适用于数据库调用,及内部方法调用。

    • pom
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    
    
    • 需要mock的方法
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Component;
    
    @Component
    @Slf4j
    public class RemoteRpc {
        public String invork(){
            log.info("远程服务调用");
            return "SUCCESS";
        }
    }
    
    
    • 单元测试
    import com.pdl.memory_database.rpc.RemoteRpc;
    import org.junit.jupiter.api.Assertions;
    import org.junit.jupiter.api.Test;
    import org.mockito.Mockito;
    import org.springframework.boot.test.mock.mockito.MockBean;
    
    public class RemoteRpcTest extends BaseTest{
        @MockBean
        private RemoteRpc remoteRpc;
    
        @Test
        void invork(){
            String result = "FAIL";
            Mockito.when(remoteRpc.invork()).thenReturn(result);
            String invork = remoteRpc.invork();
            Assertions.assertEquals(result,invork);
        }
    }
    
    

    测试方案(建议)

    使用内存数据库+spock框架+mock

    项目

    参考

    相关文章

      网友评论

          本文标题:提高单元测试能力

          本文链接:https://www.haomeiwen.com/subject/hcololtx.html