在我们写单元测试时,有一些case永远也不会失败,我们称之为快乐的单元测试。 这种case危害很大,不仅起不到质量保证的作用,还会误导开发者,以为自己的代码质量很高,场景覆盖很全。 也使得最终项目的方法覆盖率和行覆盖率都挺高,真真是表面光鲜,内里败絮。
快乐的单元测试,其中一种写法就是 – 没有断言。 当项目测试case很多,达到上千上万时,通过肉眼去找到它们难度很大,所以笔者写了一段代码,来识别没有断言的case。
思路如下:
参数为要扫描的包的路径。 通过路径先去找到该路径下,所有测试类的class文件。 对这class文件,逐一通过下面方法扫描:
-
使用 Class.forName() 加载该class文件。 再取出该类的注解, 如果有 @Ignore 注解,则忽略; 没有该注解则进行下一步; -
获取该类下的所有方法, 如果该方法 没有@Ingore注解 && 有@Test注解 && @Test注解里的expeced为none(为none说明该case没有在注解里进行异常判断), 说明该方法是个需要写断言单元测试的case; -
取出需要校验的方法后, 再使用 “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.*;
public class CheckHappyTest {
private static Logger logger = LoggerFactory.getLogger(CheckHappyTest.class);
private final String basePackage = "com.tinyv.demo.test";
private String getClassContent(String classPath){
StringBuilder sb = new StringBuilder();
String cmd = "javap -c "+classPath;
try {
Process p = Runtime.getRuntime().exec(cmd);
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();
}
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();
}
private boolean checkAssert(String mContent){
try{
return mContent.contains("invokestatic") && mContent.contains("org/junit/Assert.");
}catch (Exception e){
e.printStackTrace();
}
return false;
}
private Class getBasicClass(String name) {
Class clazz = null;
try {
clazz = Class.forName(name);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return clazz;
}
private ArrayList getJunitMethods(String classPath){
Class clazz = getBasicClass(classPath);
if(clazz==null){
return null;
}
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);
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);
}
}
|