提高单元测试能力

2021-10-11  本文已影响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
<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>

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转为对应的对象,并在方法入参中进行指定

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的标签及其作用


<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的方法时,则会返回我们需要的结果值,而不用去调用内部的业务逻辑。它不仅适用于远程服务调用,也适用于数据库调用,及内部方法调用。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

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

项目

参考

上一篇下一篇

猜你喜欢

热点阅读