扫描Junit单元测试中,没写断言的Case

在我们写单元测试时,有一些case永远也不会失败,我们称之为快乐的单元测试。

快乐的单元测试危害:
1. 起不到质量保证的作用,会误导开发者和代码维护者,错误的认为代码质量很高,场景覆盖很全。 
2. 使得项目的方法覆盖率和行覆盖率看起来很高,其实是表面光鲜,内里败絮。  

快乐的单元测试,其中一种写法就是 – 没有断言。 当项目测试case很多,达到上千上万时,通过肉眼去找到它们难度很大,所以笔者写了一段代码,来识别没有断言的case。

思路如下:

参数为要扫描的包的路径。 通过路径先去找到该路径下,所有测试类的class文件。 对这class文件,逐一通过下面方法扫描:

  1. 使用 Class.forName() 加载该class文件。 再取出该类的注解, 如果有 @Ignore 注解,则忽略; 没有该注解则进行下一步;

  2. 获取该类下的所有方法, 如果该方法 没有@Ingore注解 && 有@Test注解 && @Test注解里的expeced为none(为none说明该case没有在注解里进行异常判断), 说明该方法是个需要写断言单元测试的case;

  3. 取出需要校验的方法后, 再使用 “javap -c [classPath]” 反编译该class文件, 取出反编译后该方法的内容, 如果不包含"org/junit/Assert." , 说明该case没有使用断言;

代码如下:

package com.tinyv.demo.test.util;


import org.junit.Ignore;
import org.junit.Test;
import org.junit.platform.commons.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.*;

/**
 * @author tiny_v
 * @date 2022/3/23.
 *
 * 功能: 检查快乐的单元测试,即没有断言的断言测试
 */
public class CheckHappyTest {

    private static Logger logger = LoggerFactory.getLogger(CheckHappyTest.class);


    private final String basePackage = "com.tinyv.demo.test";

    /**
     * 执行反编译命令, 返回值为指定方法的反编译内容
     * @param classPath
     * @return
     */
    private String getClassContent(String classPath){
        StringBuilder sb = new StringBuilder();
        String cmd = "javap -c "+classPath;
        try {
            //执行命令
            Process p = Runtime.getRuntime().exec(cmd);
            //获取输出流,并包装到BufferedReader中
            BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
            String line = null;
            while ((line = br.readLine()) != null) {
                sb.append(line).append("\r\n");
            }
            p.waitFor();
        } catch(IOException e){
            e.printStackTrace();
        } catch(InterruptedException e){
            e.printStackTrace();
        }
        return sb.toString();
    }


    /**
     * 从类的反编译内容中,抽取指定方法的内容
     * @param methodName
     * @return
     */
    private String getMethodContent(String classContent, String methodName){
        StringBuilder sb = new StringBuilder();
        String[] lines = classContent.split("\r\n");
        boolean print = false;
        for(String line : lines){
            if(line.contains(methodName)){
                print = true;
            }
            if(StringUtils.isBlank(line)){
                print = false;
            }
            if(print){
                sb.append(line).append("\r\n");
            }
        }
        return sb.toString();
    }

    /**
     * 校验是否有Assert断言
     * @param mContent
     * @return
     */
    private boolean checkAssert(String mContent){
        try{
            return mContent.contains("invokestatic") && mContent.contains("org/junit/Assert.");
        }catch (Exception e){
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 加载类
     * @param name
     * @return
     */
    private Class getBasicClass(String name) {
        Class clazz = null;
        try {
            clazz = Class.forName(name);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return clazz;
    }

    /**
     * 获取需要校验的有没有断言的Case
     * 1. 如果类上加了@Ignore注解, 不需要校验
     * 2. 方法上没有@Test注解,或者加了@Ignore注解 或者 方法使用excepted进行异常断言, 不需要校验
     * @param classPath
     * @return
     */
    private ArrayList getJunitMethods(String classPath){
        Class clazz = getBasicClass(classPath);
        if(clazz==null){
            return null;
        }
        //如果类上加了@Ignore注解, 则认为不需要校验
        Annotation cIgnore = clazz.getAnnotation(Ignore.class);
        if(cIgnore!=null){
            return null;
        }
        Method[] methods = clazz.getMethods();
        if(clazz.getMethods()==null || clazz.getMethods().length==0){
            return null;
        }
        ArrayList<String> methodNames = new ArrayList();
        for (Method method : methods) {
            Annotation mAnnotation = method.getAnnotation(Test.class);
            Annotation mIgnore = method.getAnnotation(Ignore.class);
            Annotation noAssert = method.getAnnotation(NoAssert.class);
            //方法上加了@Test注解 && 方法没有使用excepted进行异常断言 && 方法没加@Ignore注解 -> 认为是需要校验的case
            if(mAnnotation!=null && mAnnotation.toString().contains("expected=class org.junit.Test$None)") && mIgnore==null && noAssert==null){
                methodNames.add(method.getName());
            }
        }
        return methodNames;
    }

    private List<String> getClassName(String packageName, boolean childPackage) {
        List<String> fileNames = null;
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        String packagePath = packageName.replace(".", "/");
        URL url = loader.getResource(packagePath);
        if (url != null) {
            String type = url.getProtocol();
            if (type.equals("file")) {
                fileNames = getClassNameByFile(url.getPath(), null, childPackage);
            }
        }
        return fileNames;
    }

    private List<String> getClassNameByFile(String filePath, List<String> className, boolean childPackage) {
        List<String> myClassName = new ArrayList<>();
        File file = new File(filePath);
        File[] childFiles = file.listFiles();
        for (File childFile : childFiles) {
            if (childFile.isDirectory()) {
                if (childPackage) {
                    myClassName.addAll(getClassNameByFile(childFile.getPath(), myClassName, childPackage));
                }
            } else {
                String childFilePath = childFile.getPath();
                if (childFilePath.endsWith(".class")) {
                    myClassName.add(childFilePath);
                }
            }
        }
        return myClassName;
    }

    private void execute() {
        String split = basePackage.split("[.]")[0];
        try {
            List<String> files = getClassName(basePackage, Boolean.TRUE);
            int m_number = 0;
            for (String file : files) {
                String classPath = (split+file.split(split)[1]).replace("\\", ".").replace(".class", "");
                ArrayList<String> methodNames = getJunitMethods(classPath);
                if(methodNames==null || methodNames.size()==0){
                    continue;
                }
                String c_content = getClassContent(file);
                logger.info("===== 类名:[{}]",  classPath);
                for(String methodName : methodNames){
                    String m_content = getMethodContent(c_content, methodName);
                    if(!checkAssert(m_content)){
                        logger.info("========== No.{}, 方法名:[{}]", ++m_number, methodName);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args){
        logger.info("============================== Start ===================================");
        long startTime = System.currentTimeMillis();
        new CheckHappyTest().execute();
        logger.info("============================== End ===================================");
        logger.info("============================== Total Cost: {} seconds", (System.currentTimeMillis()-startTime)/1000);
    }
    
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值