在我们写单元测试时,有一些case永远也不会失败,我们称之为快乐的单元测试。
复制代码
1
2
3
4快乐的单元测试危害: 1. 起不到质量保证的作用,会误导开发者和代码维护者,错误的认为代码质量很高,场景覆盖很全。 2. 使得项目的方法覆盖率和行覆盖率看起来很高,其实是表面光鲜,内里败絮。
快乐的单元测试,其中一种写法就是 – 没有断言。 当项目测试case很多,达到上千上万时,通过肉眼去找到它们难度很大,所以笔者写了一段代码,来识别没有断言的case。
思路如下:
参数为要扫描的包的路径。 通过路径先去找到该路径下,所有测试类的class文件。 对这class文件,逐一通过下面方法扫描:
-
使用 Class.forName() 加载该class文件。 再取出该类的注解, 如果有 @Ignore 注解,则忽略; 没有该注解则进行下一步;
-
获取该类下的所有方法, 如果该方法 没有@Ingore注解 && 有@Test注解 && @Test注解里的expeced为none(为none说明该case没有在注解里进行异常判断), 说明该方法是个需要写断言单元测试的case;
-
取出需要校验的方法后, 再使用 “javap -c [classPath]” 反编译该class文件, 取出反编译后该方法的内容, 如果不包含"org/junit/Assert." , 说明该case没有使用断言;
代码如下:
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192package 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("rn"); } 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("rn"); 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("rn"); } } 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); } }
最后
以上就是高大黑米最近收集整理的关于扫描Junit单元测试中,没写断言的Case的全部内容,更多相关扫描Junit单元测试中,没写断言内容请搜索靠谱客的其他文章。
本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
发表评论 取消回复