基于Java注解实现简易测试框架

2019-04-18  本文已影响0人  halfempty

1. 前言

看了点java注解, 心血来潮想撸个简易测试框架
虽然已经有JUnit / Testng这般优秀的框架存在, 但就是头铁, 觉得自己还行
于是便有了此番重复造轮子的经历, 还是破轮子

2. 构思

流程大体如下:

  1. 实例化reporter用于记录执行结果
  2. 扫描包目录并加载class
  3. 基于@Case注解, 提取class中的测试方法
  4. 基于order属性调整测试方法的排列顺序
  5. 按顺序执行测试方法, 将结果写入reporter
  6. 打印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属性, 控制用例执行的先后顺序
后续为了加强框架的能力, 可以补充额外属性, 如:

@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
上一篇下一篇

猜你喜欢

热点阅读