1. 前言
看了点java注解, 心血来潮想撸个简易测试框架
虽然已经有JUnit
/ Testng
这般优秀的框架存在, 但就是头铁, 觉得自己还行
于是便有了此番重复造轮子的经历, 还是破轮子
2. 构思
流程大体如下:
- 实例化
reporter
用于记录执行结果 - 扫描包目录并加载class
- 基于
@Case
注解, 提取class中的测试方法 - 基于
order
属性调整测试方法的排列顺序 - 按顺序执行测试方法, 将结果写入
reporter
- 打印
reporter
3. 包扫描
指定case所在的包名, 如com.lion.testcase, 或者通过入口类获取包名Application.class.getPackage().getName()
, 遍历包目录, 收集所有class
private void collectClasses(String dotPkgPath) {
if (dotPkgPath.endsWith("."))
dotPkgPath = dotPkgPath.substring(0, dotPkgPath.length() - 1);
String pkgPath = dot2path(dotPkgPath);
ClassLoader loader = Thread.currentThread().getContextClassLoader();
URL url = loader.getResource(pkgPath);
if (!url.getProtocol().equals("file"))
throw new RuntimeException("Only support local mode");
try {
String fullPkgPath = URLDecoder.decode(url.getFile(), "UTF-8");
classes.clear();
scanPkg(new File(fullPkgPath), dotPkgPath);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
private void scanPkg(File pkgFile, String dotPkgPath) {
if (!pkgFile.exists() || !pkgFile.isDirectory())
return;
File[] files = pkgFile.listFiles();
for (File f : files) {
if (f.isDirectory()) {
scanPkg(f, dotPkgPath + "." + f.getName());
continue;
}
String className = trimClassSuffix(f.getName());
String fullClassName = dotPkgPath + "." + className;
classes.put(fullClassName, null);
}
}
4. Case
注解
为了简单, 暂时只添加order
属性, 控制用例执行的先后顺序
后续为了加强框架的能力, 可以补充额外属性, 如:
- group, 用例分组
- enable, 控制用例是否执行
- depend, 用例依赖
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Case {
/**
* cases 执行顺序, 值越小优先级越高
* @return
*/
public int order() default 0;
}
5. Case收集
通过反射, 获取class下的所有方法, 如果方法标记有Case
注解, 则认为是条有效用例
private void collectCases(String className) {
try {
Class<?> clz = Class.forName(className);
Method[] methods = clz.getMethods();
for (Method method : methods) {
Annotation[] annotations = method.getDeclaredAnnotations();
for (Annotation annotation : annotations) {
if (annotation.annotationType().equals(Case.class)) {
CaseBean bean = new CaseBean();
bean.setClassname(className);
bean.setOrder(((Case) annotation).order());
bean.setMethod(method);
caseBeans.add(bean);
break;
}
}
}
} catch (ClassNotFoundException e) {
return;
}
}
以下为case样例
public class AddTest {
@Case
public void test1Add1Eq3() {
Expect.is(1+1 == 3);
}
@Case(order = 1)
public void test1Add2Eq3() {
Expect.is(1+2 == 3);
}
}
6. 基于order排序
Collections.sort(scanner.getCaseBeans(), new Comparator<CaseBean>() {
@Override
public int compare(CaseBean o1, CaseBean o2) {
return o2.getOrder() - o1.getOrder();
}
});
7. 校验手段
如何判断用例的执行结果正确与否呢, 且执行失败不能影响到后续用例的执行?
暂时只想到抛异常的方式, 通过捕捉异常到判断用户是否执行失败
这里偷懒使用了RuntimeException
, 最好还是定义自己的异常类
public class Expect {
public static void is(boolean actual) {
if(!actual)
throw new RuntimeException("校验失败");
}
}
8. 执行
通过newInstance()
实例化class, 所以只对默认构造器有效
执行method(即用例)时, 也暂不考虑参数
for(CaseBean bean: scanner.getCaseBeans()) {
String className = bean.getClassname();
if(!reporter.getResult().containsKey(className)) {
reporter.getResult().put(className, new HashMap<String, CaseStatus>());
}
Method method = bean.getMethod();
try {
Object obj = scanner.getClasses().get(className);
if(obj == null)
obj = Class.forName(className).newInstance();
method.invoke(obj);
reporter.getResult().get(className).put(method.getName(), CaseStatus.OK);
} catch (Exception e) {
reporter.getResult().get(className).put(method.getName(), CaseStatus.FAILED);
}
}
9. 结果打印
根据需要, 自己定义report的结果集, 这里不展开
后期可以丰富测试报告的呈现形式, 如使用html模板
Start at: 1555565848507
End at: 1555565849574
Total time: 0.07 s
com.lion.cases.CompareTest
test1gt3 FAILED
test10gt3 OK
com.lion.cases.AddTest
test1Add2Eq3 OK
test1Add1Eq3 FAILED
网友评论