首页 > 代码库 > Java反射学习总结终(使用反射和注解模拟JUnit单元测试框架)

Java反射学习总结终(使用反射和注解模拟JUnit单元测试框架)

转载请注明本文出自大苞米的博客(http://blog.csdn.net/a396901990),谢谢支持!

本文是Java反射学习总结系列的最后一篇了,这里贴出之前文章的链接,有兴趣的可以打开看看。

http://blog.csdn.net/a396901990/article/category/2302221

本文介绍了如何利用反射和注解去简单的模拟JUnit4单元测试框架,之所以选择JUnit4是因为4.0以后最大的改进就是使用了注解。需要注意的是这里并不是完全的模拟,只是简单实现了一下Runner类和JUnit注解相关的工作流程。所以本文的主要目的是介绍反射和注解的使用。废话不多说,直接进入正文。

首先来看一个Junit单元测试的小例子:

先定义一个简单的类,里面只有一个add计算加法的方法和一个divide计算除法的方法,divide方法需要判断除数不能为0否则抛出异常。

[java] view plaincopy
  1. public class calculate {  
  2.   
  3.     public int add(int a, int b) {  
  4.         return a + b;  
  5.     }  
  6.   
  7.     public int divide(int a, int b) throws Exception {  
  8.         if (0 == b) {  
  9.             throw new Exception("除数不能为0");  
  10.         }  
  11.         return a / b;  
  12.     }  
  13.   
  14. }  

接着写一个简单的JUnit测试类,对他进行单元测试

[java] view plaincopy
  1. import static org.junit.Assert.*;  
  2. import org.junit.After;  
  3. import org.junit.Before;  
  4. import org.junit.Test;  
  5.   
  6. public class calulateTest {  
  7.   
  8.     private calculate cal;  
  9.   
  10.     @Before     //使用JUint提供的注解标注此方法在执行测试方法前执行  
  11.     public void before() throws Exception {  
  12.         cal = new calculate();  
  13.         System.out.println("------------------");  
  14.         System.out.println("before test");  
  15.     }  
  16.   
  17.     @After      //使用JUint提供的注解标注此方法在执行测试方法后执行  
  18.     public void after() throws Exception {  
  19.         System.out.println("after test");  
  20.     }  
  21.   
  22.     @Test       //使用JUint提供的注解标注此方法为需要测试的方法  
  23.     public void addTest() {  
  24.         System.out.println("do add test");  
  25.         int result = cal.add(1020);  
  26.         //判断result和预期的值是否相等,在此例中如果result等于30则测试通过  
  27.         assertEquals(30, result);  
  28.     }  
  29.       
  30.     @Test(expected = Exception.class)  //使用JUnit的Test注解,并且判断预期值是否是Exception  
  31.     public void div() throws Exception {  
  32.         System.out.println("do divide test");  
  33.         cal.divide(10);   //调用1除以0,抛出异常  
  34.     }  
  35.   
  36. }  

执行结果为:

before test
do add test
after test
------------------
before test
do divide test
after test


下面我们就用反射和注解的知识来模拟JUnit对于上面例子的实现。

这里先不着急看代码,先看梳理一下思路。

1.JUnit只可以知道一件事,那就是待测试类的名字,其他的一概不知。所以我们只能利用测试类的名字作为切入口

2.通过测试类的名字,使用反射去获取他的Class对象

3.然后通过该Class对象获得当前类中所有方法的Method数组

4.遍历这个Method数组,取得每一个Method对象

5.调用每一个Method对象的isAnnotationPresent(Annotation.class)方法,判断该方法是否被指定注解所修饰

6.本例中根据不同的注解,来判断调用方法的顺序。

7.如果Test注解有属性的话,则判断方法执行后的返回值,如果返回值等于预期的注解属性也就是expected = Exception.class则测试通过。

8.最后还有一个assertEquals方法,他去判断预期值和实际值是否相等来决定测试是否通过。


大致的思路有了,我们就可以开始模拟它了。

首先定义3个注解,分别是Before,Test,After。如果对于定义注解不清楚的同学请看我之前写的文章。

[java] view plaincopy
  1. @Target(ElementType.METHOD)  
  2. @Retention(RetentionPolicy.RUNTIME)  
  3. public @interface Before {  
  4.       
  5. }  
[java] view plaincopy
  1. @Target(ElementType.METHOD)  
  2. @Retention(RetentionPolicy.RUNTIME)  
  3. public @interface Test {  
  4.     Class<? extends Object> expected() default String.class;  
  5. }  
[java] view plaincopy
  1. @Target(ElementType.METHOD)  
  2. @Retention(RetentionPolicy.RUNTIME)  
  3. public @interface After {  
  4.   
  5. }  
三个很简单的注解,都标注只能修饰方法,保留策略为运行时,这样可以被反射读取到。

只有Test注解中定义了一个属性,类型可以为任何类型的Class对象,默认值为String类型的Class对象。


接下来定义我们模拟的JUnit类,这里为了方便我将所有能用到的都写在一个MyJUnit类中。他对外只有一个构造方法和一个run方法。还有一个对比用的assertEquals方法

[java] view plaincopy
  1. public class MyJUnit {  
  2.   
  3.     // 存放所有标注了before注解的集合  
  4.     private List<Method> beforeMethod;  
  5.     // 存放所有标注了after注解的集合  
  6.     private List<Method> afterMethod;  
  7.     // 存放所有标注了test注解的集合  
  8.     private List<Method> testMethod;  
  9.     // 存放需要捕捉的异常集合  
  10.     private static List<Exception> exceptions;  
  11.     // 被测试类的实例化对象  
  12.     private Object object;  
  13.     // 被测试类的Class对象  
  14.     private Class<?> testClass;  
  15.   
  16.     //自定义MyJUnit类的构造方法,用于接收被测试类的名字然后初始化需要的变量和方法  
  17.     public MyJUnit(String testName) {  
  18.         super();  
  19.         try {  
  20.             beforeMethod = new ArrayList<>();  
  21.             afterMethod = new ArrayList<>();  
  22.             testMethod = new ArrayList<>();  
  23.             exceptions = new ArrayList<>();  
  24.               
  25.             //使用反射根据传递的类名生成对象  
  26.             testClass = Class.forName(testName);  
  27.             object = testClass.newInstance();  
  28.   
  29.             //获取所有的方法并根据注解进行分类  
  30.             getAllMethods();  
  31.               
  32.         } catch (Exception e) {  
  33.             e.printStackTrace();  
  34.         }  
  35.     }  
  36.   
  37.     // 根据注解获取所有的方法  
  38.     private void getAllMethods() {  
  39.         Method[] methods = testClass.getMethods();  
  40.         for (Method m : methods) {  
  41.             // 找到被before修饰的方法,放入before方法的集合中  
  42.             if (m.isAnnotationPresent(Before.class)) {  
  43.                 beforeMethod.add(m);  
  44.             }  
  45.             // 找到被After修饰的方法,放入after方法的集合中  
  46.             if (m.isAnnotationPresent(After.class)) {  
  47.                 afterMethod.add(m);  
  48.             }  
  49.             // 找到被test修饰的方法,放入test方法的集合中  
  50.             if (m.isAnnotationPresent(Test.class)) {  
  51.                 testMethod.add(m);  
  52.             }  
  53.         }  
  54.     }  
  55.   
  56.     // run方法  
  57.     public void run() {  
  58.         // 运行所有的测试方法  
  59.         for (Method method : testMethod) {  
  60.             runTest(method);  
  61.         }  
  62.   
  63.         // 判断捕捉的异常集合,如果大小为0则没有异常,测试通过。有异常则表示测试不通过并打印异常信息  
  64.         if (exceptions.size() == 0) {  
  65.             System.out.println("通过测试");  
  66.         } else {  
  67.             for (Exception e : exceptions) {  
  68.                 System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");  
  69.                 System.out.println("测试不通过,错误的内容为:");  
  70.                 e.printStackTrace();  
  71.             }  
  72.         }  
  73.     }  
  74.   
  75.     // 按照before test after的顺序执行  
  76.     private void runTest(Method method) {  
  77.         try {  
  78.             runBefores();  
  79.             runTestMethod(method);  
  80.             runAfters();  
  81.         } catch (Exception e) {  
  82.             e.getMessage();  
  83.             throw new RuntimeException(  
  84.                     "test should never throw an exception to this level");  
  85.         }  
  86.     }  
  87.   
  88.     // 执行所有after标注的方法  
  89.     private void runAfters() throws Exception {  
  90.         for (Method m : afterMethod) {  
  91.             m.invoke(object, new Object[] {});  
  92.         }  
  93.     }  
  94.   
  95.     // 执行所有before标注的方法  
  96.     private void runBefores() throws Exception {  
  97.         for (Method m : beforeMethod) {  
  98.             m.invoke(object, new Object[] {});  
  99.         }  
  100.     }  
  101.   
  102.     // 执行test方法  
  103.     private void runTestMethod(Method method) {  
  104.         // 判断测试是否通过的标志  
  105.         boolean passCheck = false;  
  106.         try {  
  107.             // 获得Test注解  
  108.             Test testAnnotation = method.getAnnotation(Test.class);  
  109.             // 判断test注解标注的属性是否为需要的类型,这里可以根据需要加入不同的判断条件  
  110.             if (testAnnotation.expected().newInstance() instanceof Exception) {  
  111.                 passCheck = true;  
  112.             }  
  113.             method.invoke(object);  
  114.         } catch (Exception e) {  
  115.             // 判断通过则测试通过,否者将异常加入到异常集合中  
  116.             if (passCheck) {  
  117.                 return;  
  118.             } else {  
  119.                 addExceptions(e);  
  120.             }  
  121.         }  
  122.     }  
  123.   
  124.     private static void addExceptions(Exception e) {  
  125.         exceptions.add(e);  
  126.     }  
  127.   
  128.     // 自定义的assertEquals方法,判断两个值是否相等,可以根据需要加入更多类型的判断,如果相等则通过,否则new一个异常加入到异常集合中  
  129.     static public void assertEquals(Object expected, Object actual) {  
  130.         if (expected.equals(actual)) {  
  131.             return;  
  132.         } else {  
  133.             addExceptions(new Exception("预期值与实际值不相等"));  
  134.         }  
  135.     }  
  136. }  
注解和JUnit类都定义好后可以写测试的方法了,和之前的测试方法没有区别,只是这次导包导入的都是我们自定义的包。

[java] view plaincopy
  1. import static gxy.test.Junit.MyJUnit.*;  
  2.   
  3. public class MyCalulateTest {  
  4.   
  5.     private Calculate cal;  
  6.   
  7.     @Before  
  8.     public void before() throws Exception {  
  9.         cal = new Calculate();  
  10.         System.out.println("------------------");  
  11.         System.out.println("before test");  
  12.     }  
  13.   
  14.     @After  
  15.     public void after() throws Exception {  
  16.         System.out.println("after test");  
  17.     }  
  18.   
  19.     @Test  
  20.     public void addTest() {  
  21.         System.out.println("do add test");  
  22.         int result = cal.add(1020);  
  23.         // 这里的预期值为40,实际为30,所以这个方法通过不了测试  
  24.         assertEquals(40, result);  
  25.     }  
  26.   
  27.     @Test(expected = Exception.class)  
  28.     public void divTest() throws Exception {  
  29.         System.out.println("do divide test");  
  30.         // 调用1除以0,抛出异常  
  31.         cal.divide(10);  
  32.     }  
  33.   
  34. }  
为了检验测试效果,这里对于addTest的方法中assertEquals方法传入的预期值和实际值不同。

下面看最后的运行类。

[java] view plaincopy
  1. public static void main(String[] args) throws Exception {  
  2.         MyJUnit myJUnit = new MyJUnit("gxy.test.Junit.MyCalulateTest");  
  3.         myJUnit.run();  
  4.     }  
只有2行代码,传入需要测试的类的名字,然后执行run方法。

测试结果:

------------------

before test

do add test
after test
------------------
before test
do divide test
after test
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
测试不通过,错误的内容为:
java.lang.Exception: 预期值与实际值不相等
at gxy.test.Junit.MyJUnit.assertEquals(MyJUnit.java:139)
at gxy.test.Junit.MyCalulateTest.addTest(MyCalulateTest.java:26)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at gxy.test.Junit.MyJUnit.runTestMethod(MyJUnit.java:119)
at gxy.test.Junit.MyJUnit.runTest(MyJUnit.java:85)
at gxy.test.Junit.MyJUnit.run(MyJUnit.java:66)
at gxy.test.Junit.FinalTest.main(FinalTest.java:13)

本文只是简单的模拟了一下在JUnit中反射和注解的使用,而且在很多框架中很多都利用了反射和注解这对黄金组合来实现一些如权限判断,调用等等很多功能。所以说反射还是值得好好学习和研究的。

反射学习总结系列博文断断续续写了一个多月,这篇是最后一篇了。通过这一个月的学习对反射的基本概念和使用算是有了一个了解,有时间还需要深入的学习。

这里需要提一下,学习的资料主要是从网上下的系列视频。主要借鉴了其中中的思路和一些概念类的东西,但是文章中的例子都是我自己写的。最后向大家推荐一下这个视频吧,不是做广告,讲的确实不错,讲课的老师叫张龙,口齿清晰讲的很深入。在大学时看的马士兵的视频,比较适合入门,这个适合晋级。再想继续晋级就得看书了,哈哈。

由于一些烂七八糟的原因我就不提供这个视频的下载地址了,如果需要请自己上网搜,或者留下邮箱我给链接发过去。

最后把本例的代码上传了,导入就可以运行。点击打开下载链接