根据单元测试代码导出测试用例文档

最近得到了一个新的需求,如何通过单元测试代码自动导出测试用例文档,我大概网上搜了搜,并没有什么现成的比较好用的工具,于是决定自己写代码来实现该需求

1.思路

主要使用的就是反射,通过反射获取测试类中的方法信息,以及一系列自定义的注解,生成用例文档

2.使用说明

单元测试示例代码如下

@UnitName("测试管理1")
public class Test1 {

    private static final Logger log = LoggerFactory.getLogger(Test1.class);

    @Mock
    private ClassesEsServiceImpl classesEsService;

    @InjectMocks
    private ClassesServiceImpl classesService;

    @BeforeEach
    void setup() {
        //初始化
        MockitoAnnotations.openMocks(this);
    }

    @Test
    @CaseType("等价类")
    @CaseDesignMethod("语句覆盖")
    void testGet_BASE_04_001_Test_1() {
        when(classesEsService.queryAll()).thenReturn(new ArrayList<>());
        classesService.queryAll();
        log.info("param: test_1_param");
        log.info("result: test_1_result");
    }
    
    @Test
    @CaseType("等价类")
    @CaseDesignMethod("语句覆盖")
    void testGet_BASE_04_001_Test_2() {
        List<String> list = Arrays.asList("mai", "da", "da", "wlx", "hello world");
        log.info("param: " + JSON.toJSONString(list));
        log.info("result: test1_2");
    }

示例代码未给全,导出excel示例如下
在这里插入图片描述
以存在值的这几个字段为例,讲解一下获取的原理:其中用例编号、Unit ID、Test Case Name都是从测试方法名中获取的,像方法名 “testGet_BASE_04_001_Test_1”,用例编号就是Test_1,Unit ID是BASE_04_001,Test Case Name是类名.Get,具体如何处理这几个值是下方代码实现中的processMethodName中处理的,可以根据自身需求处理。而测试用例方法和用例类型是从注解中获取的值,输入参数和预期结果则是通过打印日志到控制台,然后拦截日志内容,根据日志字符串处理获取的,解析日志字符串需要一定的规则,我就是以 “result:” 后面的值为预期结果,“param:” 后面的值为输入参数,注意冒号是英文冒号,最后sheet名是从类上的自定义注解@UnitName中获取的

3.代码实现

首先是自定义的几个注解,可以根据自身需求来决定注解

/**
 * 测试用例设计方法
 *
 * @author WuLinXuan
 * @date 2024-01-30 15:12
 */
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CaseDesignMethod {

    String value() default "语句覆盖";
}

/**
 * 用例类型
 *
 * @author WuLinXuan
 * @date 2024-01-30 15:12
 */
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CaseType {

    String value() default "等价类";
}

/**
 * 单元名称
 *
 * @author WuLinXuan
 * @date 2024-01-30 15:19
 */
@Target(value = ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UnitName {

    String value();
}

Excel导出模版类,导出使用的是EasyExcel,根据自身需求和对应文档格式决定要有哪些字段,我也只给出了对应文档部分字段

@Data
public class TestCase {

    @ExcelProperty("用例编号")
    private String caseNumber;

    @ExcelProperty("Unit ID")
    private String unitID;

    @ExcelProperty("Test Case Name")
    private String caseName;

    @ExcelProperty("测试用例设计方法")
    private String caseDesignMethod;

    @ExcelProperty("用例类型")
    private String caseType;

    @ExcelProperty("输入参数")
    private String inputParam;
}

注解扫描处理类,运行main方法即可,注意该类会扫描该类所在包下所有的包下的类,也就是说和该类同级的类和上级的类不会扫描

/**
 * 测试注解扫描类
 *
 * @author WuLinXuan
 * @date 2024-01-30 16:22
 */
public class Test {

	// 导出sheet编号,每一个测试类为一个sheet
    public static int sheetCount = 0;
	// 导出excel文件路径
    public static String fileOutputPath = "d:/desktop/doc/test/test.xlsx";

    public static String line = "---------------------";

    public static void main(String[] args) throws Exception{

        // 获取当前类所在文件夹
        String packagePath = Test.class.getResource("").getPath();
        packagePath = URLDecoder.decode(packagePath, "UTF-8").replace('.', '\\');
        File packageDir = new File(packagePath);

        // 与该类同级的类及自身不考虑,只选择文件夹,获取Class
        List<Class<?>> classList = new ArrayList<>();
        for (File file : packageDir.listFiles()) {
            if (file.isDirectory()) {
                classList.addAll(getClassFiles(file, file.getName() + "."));
            }
        }

        processClassList(classList);
    }

    /**
     * 递归获取文件夹下的类Class
     *
     * @param dir
     * @param packageName
     * @return
     * @throws Exception
     */
    public static List<Class<?>> getClassFiles(File dir, String packageName) throws Exception{
        List<Class<?>> list = new ArrayList<>();
        if (!dir.isDirectory()) {
            return list;
        }
        for (File file : dir.listFiles()) {
            if (file.isFile()) {
                String className = packageName + file.getName();
                // build过后的类文件是以 class 结尾,去掉就是类名
                className = className.substring(0, className.lastIndexOf(".class"));
                list.add(Class.forName(className));
            } else {
                packageName = packageName + file.getName() + ".";
                list.addAll(getClassFiles(file, packageName));
            }
        }
        return list;
    }

    /**
     * 处理Class集合
     *
     * @param classList
     * @throws Exception
     */
    public static void processClassList(List<Class<?>> classList) throws Exception{
        // 输出文件位置
        FileOutputStream fileOutputStream = new FileOutputStream(fileOutputPath);
        ExcelWriter excelWriter = EasyExcel.write(fileOutputStream).build();
        for (Class<?> clazz : classList) {
            System.out.println(line + clazz.getName() + line);
            UnitName unitName = clazz.getAnnotation(UnitName.class);
            String sheetName = unitName.value();
            List<TestCase> caseList = new ArrayList<>();
            Object instance = clazz.getConstructor().newInstance();

            
            Method[] methods = clazz.getDeclaredMethods();
            // 根据方法名中的数字排序,因为默认获取的方法数组是无序的
            Arrays.sort(methods, (Comparator.comparing(o -> getMethodNameNumber(o.getName()))));

            for (Method method : methods) {
                // 方法名需以 test 开头
                if (method.getName().indexOf("test") != 0) {
                    continue;
                }
                // 开启mock模拟,防止调用单元测试方法报错,实际上是为类中注入有@Mock、@InjectionMocks注解的类
                MockitoAnnotations.openMocks(instance);

                // 调用方法
                String capturedLog = invokeAndCaptureLog(method, instance);

                TestCase testCase = new TestCase();
                // 日志中存在关键字 param: 和 result: ,则将其后面的字符串视为需要设置的值,注意是英文冒号
                String[] logs = capturedLog.split("\n");
                for (String s : logs) {
                    int i = s.indexOf("param:");
                    int j = s.indexOf("result:");
                    if (i != -1) {
                        testCase.setInputParam(s.substring(i + 6).trim());
                    } else if(j != -1) {
                        testCase.setExpectResult(s.substring(j + 7).trim());
                    }
                }

                caseList.add(processMethodName(method, clazz, testCase));

            }
            exportList(caseList, sheetName, excelWriter);
        }
        excelWriter.finish();
    }

    /**
     * 获取方法名中的数字字符串
     *
     * @param methodName
     * @return
     */
    public static String getMethodNameNumber(String methodName) {
        Pattern pattern = Pattern.compile("\\d+");
        Matcher matcher = pattern.matcher(methodName);
        StringBuilder numbers = new StringBuilder();
        while (matcher.find()) {
            numbers.append(matcher.group());
        }
        return numbers.toString();
    }

    /**
     * 调用方法并拦截log日志内容返回
     *
     * @param method
     * @param instance
     * @return
     * @throws Exception
     */
    public static String invokeAndCaptureLog(Method method, Object instance) throws Exception{
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        PrintStream printStream = new PrintStream(byteArrayOutputStream);
        PrintStream originalOut = System.out;
        System.setOut(printStream);

        method.setAccessible(true);
        method.invoke(instance);

        System.setOut(originalOut);
        return byteArrayOutputStream.toString();
    }

    /**
     * 根据方法名解析对应属性值
     * 以 testGet_BASE_04_001_Test_1 为例, 这是一个标准测试用例方法名,首先去除test,然后Base_04_001为Unit ID,Get为测试方法名,
     * Test_1为用例编号
     *
     * @param method
     * @param clazz
     * @param testCase
     * @return
     */
    public static TestCase processMethodName(Method method, Class<?> clazz, TestCase testCase) {
    	// 获取被测试的类名,通常单元测试中该类为打上注解@InjectMocks的类,如有其他特殊情况请自行修改
    	String testClassName = "";
        for (Field field : clazz.getDeclaredFields()) {
            field.setAccessible(true);
            InjectMocks annotation = field.getAnnotation(InjectMocks.class);
            if (annotation != null) {
                testClassName = field.getType().getSimpleName();
            }
        }
        
        String methodName = method.getName();
        // 去除方法名前的 test
        methodName = methodName.substring(4);
        String[] split = methodName.split("_");
        CaseType caseType = method.getAnnotation(CaseType.class);
        CaseDesignMethod caseDesignMethod = method.getAnnotation(CaseDesignMethod.class);

        testCase.setCaseNumber(String.join("_", split[4], split[5]));
        testCase.setUnitID(String.join("_", split[1], split[2], split[3]));
        testCase.setCaseName(testClassName + "." + split[0]);
        testCase.setCaseDesignMethod(caseDesignMethod.value());
        testCase.setCaseType(caseType.value());

        System.out.println(method.getName());
        return testCase;
    }

    /**
     * excel导出
     *
     * @param caseList
     * @param sheetName
     * @param excelWriter
     * @throws Exception
     */
    public static void exportList(List<TestCase> caseList, String sheetName, ExcelWriter excelWriter) throws Exception{
        WriteSheet writeSheet = EasyExcel.writerSheet(sheetCount++, sheetName).head(TestCase.class).build();
        excelWriter.write(caseList, writeSheet);
    }
}

4.结尾

注意使用这些代码的前提是编写测试用例时要按照一定的规范编写:测试类上打好@UnitName注解值作为sheet名等,方法上打好注解@CaseType等注解,单元测试方法内部需要使用log并且内容按照一定规则。注意各个测试用例文档格式可能不同,需要自己对代码进行调整,我也并未对一些特殊情况进行处理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值