我们在开发项目的过程中遇到了复杂的业务需求,测试同学没有办法帮我们覆盖每一个场景;或者是我们自己在做代码功能升级、技改,而不是业务需求的时候,可能没有测试资源帮我们做测试,那这个时候就需要依靠自己的单元测试来保证产品的质量。
我们的工程一般分为接口层,业务层,仓储层;那每一个模块都需要我们用单元测试来覆盖。
仓储层:这一层,我们一般会连接到真实的数据库,完成仓储层的CRUD,我们可以连到开发库或者测试库,但是仅仅是单元测试就需要对我们资源占用,成本有点高,所以h2基于内存的数据库就很好的帮我们解决这些问题。
业务层:业务层的逻辑比较复杂,我们可以启动整个服务帮助测试,也可以使用mock来覆盖每一个分支,因为用mock的话不需要启动服务,专注我们的业务流程,更快也更方便。
接口层:一般接口层我们会用集成测试的较多,启动整个服务端到端的流程捋下来,采用BDD的思想,给什么入参,期望什么结果,写测试用例的时候只是专注于入参出参就行,测试代码不用做任何改变。
首先junit4和junit5都支持参数化的测试,但我用下来感觉到内置的这些功能不能够满足我的需求,所以我一般会自定义数据类型。
下面以一个controller接口为例完成集成测试:
采用springboot+mybatisplus完成基础功能,代码忽略,只贴一下controller和配置文件
@RestController
@RequiredArgsConstructor
public class UserController {
    private final UserService userService;
    @GetMapping("/query")
    public UserDO query(String username) {
        LambdaQueryWrapper<UserDO> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(UserDO::getUsername, username);
        return userService.getOne(queryWrapper);
    }
}spring:
  application:
    name: fiat-exchange
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/maple?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=convertToNull
    username: root
    password: ''
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl首先我们上面说集成测试需要启动整个服务,DB采用h2的基于内存的数据库,同时需要初始化库与表
spring:
  profiles:
    active: test
  application:
    name: integration-testing-local-test
  datasource:
    # 测试用的内存数据库,模拟MSQL
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:test;mode=mysql
    username: root
    password: test
  sql:
    init:
      schema-locations: classpath:schema.sql
      data-locations: classpath:data.sql
      mode: always
schema.sql
DROP TABLE `user` IF EXISTS;
CREATE TABLE `user` (
    `username` varchar(64) COMMENT 'username',
    `password` varchar(64) COMMENT 'password'
) ENGINE=InnoDB DEFAULT CHARSET = utf8 COMMENT='user';data.sql
INSERT INTO `user` (`username`, `password`) VALUES ('maple', '123456');
INSERT INTO `user` (`username`, `password`) VALUES ('cc', '654321');IntegrationTestingApplicationTests
package com.maple.integration.testing;
import com.maple.integration.testing.controller.UserController;
import com.maple.integration.testing.entity.UserDO;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(classes = IntegrationTestingApplication.class)
class IntegrationTestingApplicationTests {
    @Autowired
    private UserController userController;
    @Test
    void contextLoads() {
        UserDO userDO = userController.query("maple");
        assertThat(userDO.getPassword()).isEqualTo("123456");
    }
}
工程结构

单元测试可以跑通了,但是如果要加测试用例的话就需要再加代码加用例,不符合我们的要求,我们要求能有一个地方放入参出参就行,下面我们改造下。
1、BaseTestData
基类,任何测试bean都需要集成它
@Data
public abstract class BaseTestData {
    private String testCaseName;
    private Object[] expectedResult;
}2、 UserDTOTestData
@Data
@EqualsAndHashCode(callSuper = true)
public class UserDTOTestData extends BaseTestData {
    private String username;
}3、JsonFileSource
@ParameterizedTest 使用junit5的参数化测试的主键,他内置了一些功能注解,比如:MethodSource、EnumSource、CsvFileSource等,我们参考内置的来自定义JsonFileSource,可以测试单个用例,也可以扫描文件路径测试批量用例
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@API(
        status = API.Status.EXPERIMENTAL,
        since = "5.0"
)
@ArgumentsSource(JsonFileArgumentsProvider.class)
public @interface JsonFileSource {
    /**
     * 文件路径:controller.userController.query/
     * @return
     */
    String directoryPath() default "";
    /**
     * 具体的文件路径:/controller.userController.query/validCase_QueryUser.json
     * @return
     */
    String[] resources() default "";
    String encoding() default "UTF-8";
    Class<?> typeClass();
}4、JsonFileArgumentsProvider
package com.beet.fiat.config;
import com.alibaba.fastjson.JSON;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.support.AnnotationConsumer;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.commons.util.StringUtils;
import org.springframework.util.StreamUtils;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.function.BiFunction;
import java.util.stream.Stream;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
 * @author maple.wang
 * @date 2022/11/17 16:24
 */
public class JsonFileArgumentsProvider implements ArgumentsProvider, AnnotationConsumer<JsonFileSource> {
    private final BiFunction<Class<?>, String, InputStream> inputStreamProvider;
    private String[] resources;
    private String directoryPath;
    private String encoding;
    private Class<?> typeClass;
    private static final ClassLoader CLASS_LOADER = JsonFileArgumentsProvider.class.getClassLoader();
    public JsonFileArgumentsProvider() {
        this(Class::getResourceAsStream);
    }
    public JsonFileArgumentsProvider(BiFunction<Class<?>, String, InputStream> inputStreamProvider) {
        this.inputStreamProvider = inputStreamProvider;
    }
    @Override
    public void accept(JsonFileSource jsonFileSource) {
        this.directoryPath = jsonFileSource.directoryPath();
        this.resources = jsonFileSource.resources();
        this.encoding = jsonFileSource.encoding();
        this.typeClass = jsonFileSource.typeClass();
    }
    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) throws Exception {
        String displayName = extensionContext.getDisplayName();
        System.out.println(displayName);
        if(StringUtils.isNotBlank(directoryPath)){
            List<String> resourcesFromDirectoryPath = getResources(directoryPath);
            String[] resourcesArrayFromDirectoryPath = Optional.of(resourcesFromDirectoryPath).orElse(null).toArray(String[]::new);
            if(Objects.nonNull(resourcesArrayFromDirectoryPath) && resourcesArrayFromDirectoryPath.length > 0){
                resources = ArrayUtils.addAll(resourcesArrayFromDirectoryPath, resources);
            }
        }
        return Arrays.stream(resources)
                .filter(StringUtils::isNotBlank)
                .map(resource -> openInputStream(extensionContext, resource))
                .map(this::createObjectFromJson)
                .map(str -> JSON.parseObject(str, typeClass))
                .map(Arguments::of);
    }
    private List<String> getResources(String directoryPath) throws IOException{
        List<String> testFileNames;
        try (InputStream directoryStream = CLASS_LOADER.getResourceAsStream(directoryPath)) {
            if (directoryStream == null) {
                return List.of();
            }
            testFileNames = IOUtils.readLines(directoryStream, UTF_8);
        }
        // for each file found, parse into TestData
        List<String> testCases = new ArrayList<>();
        for (String fileName : testFileNames) {
            Path path = Paths.get(directoryPath, fileName);
            testCases.add("/" + path);
        }
        return testCases;
    }
    private String createObjectFromJson(InputStream inputStream) {
        try {
            return StreamUtils.copyToString(inputStream, Charset.forName(encoding));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
    private InputStream openInputStream(ExtensionContext context, String resource) {
        Preconditions.notBlank(resource, "Classpath resource [" + resource + "] must not be null or blank");
        Class<?> testClass = context.getRequiredTestClass();
        return Preconditions.notNull(inputStreamProvider.apply(testClass, resource),
                () -> "Classpath resource [" + resource + "] does not exist");
    }
}
IntegrationTestingApplicationTests 就变为了
    @ParameterizedTest
    @JsonFileSource(resources = "/controller.userController.query/validCase_QueryUser.json", typeClass = UserDTOTestData.class)
    @DisplayName("query user")
    void queryUser(UserDTOTestData testData) {
        UserDO userDO = userController.query(testData.getUsername());
        assertThat(userDO.getPassword()).isEqualTo(testData.getExpectedResult()[0]);
    }
    @ParameterizedTest
    @JsonFileSource(directoryPath = "controller.userController.query/", typeClass = UserDTOTestData.class)
    @DisplayName("query user")
    void queryUsers(UserDTOTestData testData) {
        UserDO userDO = userController.query(testData.getUsername());
        assertThat(userDO.getPassword()).isEqualTo(testData.getExpectedResult()[0]);
    }






![[附源码]计算机毕业设计基于微信小程序的网络办公系统Springboot程序](https://img-blog.csdnimg.cn/439d6a91c09f4bf08d13951ff3a7f12f.png)











