Junit是非常短小精悍的单元测试框架,里面用到了大量的设计模式和设计原则,当然本文不是去分析这些模式,只是从头看代码分析一下它的执行过程:
1.Junit38ClassRunner的构造方法
public JUnit38ClassRunner(Class<?> klass) { this(new TestSuite(klass.asSubclass(TestCase.class))); }
2.TestSuite的构造方法
public TestSuite(final Class<?> theClass) { addTestsFromTestCase(theClass); }
下面看一下addTestsFromTestCase(theClass)方法的源码
private void addTestsFromTestCase(final Class<?> theClass) { fName= theClass.getName(); try { getTestConstructor(theClass); // Avoid generating multiple error messages } catch (NoSuchMethodException e) { addTest(warning("Class "+theClass.getName()+" has no public constructor TestCase(String name) or TestCase()")); return; } if (!Modifier.isPublic(theClass.getModifiers())) { addTest(warning("Class "+theClass.getName()+" is not public")); return; } Class<?> superClass= theClass; List<String> names= new ArrayList<String>(); while (Test.class.isAssignableFrom(superClass)) { for (Method each : superClass.getDeclaredMethods()) addTestMethod(each, names, theClass); superClass= superClass.getSuperclass(); } if (fTests.size() == 0) addTest(warning("No tests found in "+theClass.getName())); }
然后我们再来看看addTestMethod方法
private void addTestMethod(Method m, List<String> names, Class<?> theClass) { String name= m.getName(); if (names.contains(name)) return; if (! isPublicTestMethod(m)) { if (isTestMethod(m)) addTest(warning("Test method isn't public: "+ m.getName() + "(" + theClass.getCanonicalName() + ")")); return; } names.add(name); addTest(createTest(theClass, name)); }
isTestMethod(Method m)方法判断该方法是否为测试方法,即以test开头或者有相关注解的方法
addTest(createTest(theClass, name));这里将创建出TestCase的实例,并添加到fTests(Vector)中
这里创建的出的TestCase的fname属性将保存实际要测试的方法名
3.TestSuite的createTest方法
static public Test createTest(Class<?> theClass, String name) { Constructor<?> constructor; try { constructor= getTestConstructor(theClass); } catch (NoSuchMethodException e) { return warning("Class "+theClass.getName()+" has no public constructor TestCase(String name) or TestCase()"); } Object test; try { if (constructor.getParameterTypes().length == 0) { test= constructor.newInstance(new Object[0]); if (test instanceof TestCase) ((TestCase) test).setName(name); } else { test= constructor.newInstance(new Object[]{name}); } } catch (InstantiationException e) { return(warning("Cannot instantiate test case: "+name+" ("+exceptionToString(e)+")")); } catch (InvocationTargetException e) { return(warning("Exception in constructor: "+name+" ("+exceptionToString(e.getTargetException())+")")); } catch (IllegalAccessException e) { return(warning("Cannot access test case: "+name+" ("+exceptionToString(e)+")")); } return (Test) test; }
下面我们进入Test的执行过程:
4.Junit38ClassRunner的run方法
public void run(RunNotifier notifier) { TestResult result= new TestResult(); result.addListener(createAdaptingListener(notifier)); getTest().run(result); }
这里getTest()的得到的是TestSuite,上面的代码我们已经知道TestSuite中包含了多个TestCase实例,当然这里说的比较局限TestSuite也可以包含其他的TestSuite去执行一组测试,它们都保存在fTests中,这其实是组合模式,那接着我们就应该看看TestSuite的run方法
5.TestSuite的run方法
public void run(TestResult result) { for (Test each : fTests) { if (result.shouldStop() ) break; runTest(each, result); } }
不出所料,在这里是遍历fTests去执行每一个TestCase实例的run方法,那我们就继续去看TestCase的run方法
6.TestCase的run方法
public void run(TestResult result) { result.run(this); }
这里TestCase把自己作为参数传给TestResult的run方法去执行,这里为什么要这样做呢,为什么不直接执行呢?这个问题先留在这里,接着看
7.TestResult的run方法
protected void run(final TestCase test) { startTest(test); Protectable p= new Protectable() { public void protect() throws Throwable { test.runBare(); } }; runProtected(test, p); endTest(test); }
startTest、endTest方法无非就是做一些初始化和销毁的工作,就不进入每个方法去看了,TestResult顾名思义是用来收集测试结果的,它在Runner、TestSuite、TestCase之间一直传递,保存所有测试方法的执行结果
这里创建了一个受保护的Protectable类,在里面执行TestCase的runBare方法,runProtected()会去调用p.protect()方法.这说明测试方法实际上还是在TestCase中执行的,那为什么要传给TestResult,再在TestResult类中调用TestCase的runBare方法呢,我们来看一下runProtected()的具体过程吧
8.TestResult的runProtected方法
public void runProtected(final Test test, Protectable p) { try { p.protect(); } catch (AssertionFailedError e) { addFailure(test, e); } catch (ThreadDeath e) { // don't catch ThreadDeath by accident throw e; } catch (Throwable e) { addError(test, e); } }
很明显,这个过程是为了收集执行结果的,如果catch到AssertionFailureError表示测试结果不正确,如果catch到其他异常表示测试出现错误,addFailure和addError方法其实就是往TestResult的fFailures和fErrors列表中添加对象,并通知相应的观察者。
我们回到主过程中,来接着看测试方法是如何被调用的:
9.TestCase的runBare()方法
public void runBare() throws Throwable { Throwable exception= null; setUp(); try { runTest(); } catch (Throwable running) { exception= running; } finally { try { tearDown(); } catch (Throwable tearingDown) { if (exception == null) exception= tearingDown; } } if (exception != null) throw exception; }
这里我们就会发现为什么junit在对每个方法进行测试时都要先后执行setUp()、实际要测试的方法和tearDown()方法,这里用的模板方法模式,setUp和tearDown如果在测试类中有进行复写,那实际执行的是测试类中的setUp和tearDown,而且测试每个类的前后都会执行这两个方法,而实际执行要测试的方法是在runTest()中实现的,下面就来看看runTest()方法吧
10.TestCase的runTest()方法
protected void runTest() throws Throwable { assertNotNull("TestCase.fName cannot be null", fName); // Some VMs crash when calling getMethod(null,null); Method runMethod= null; try { // use getMethod to get all public inherited // methods. getDeclaredMethods returns all // methods of this class but excludes the // inherited ones. runMethod= getClass().getMethod(fName, (Class[])null); } catch (NoSuchMethodException e) { fail("Method \""+fName+"\" not found"); } if (!Modifier.isPublic(runMethod.getModifiers())) { fail("Method \""+fName+"\" should be public"); } try { runMethod.invoke(this); } catch (InvocationTargetException e) { e.fillInStackTrace(); throw e.getTargetException(); } catch (IllegalAccessException e) { e.fillInStackTrace(); throw e; } }
这里就可以发现Junit实际上就是把我们要测试的方法反射出来去invoke,以上这些就是Junit进行测试的全过程了,下面我们再来看分析前面的过程中遗留的问题:
为什么在TestCase的run方法中不直接执行测试方法,而要将自身传给TestResult的run方法中去执行?
这个问题通过后面的执行过程,大家应该也看明白了,这其实是Junit的设计上非常值得我们学习的地方,就是类的单一职责特点,TestCase里的方法应该都是测试相关的,而TestResult里的方法都是为了结果服务的,这个交织的过程就是为了获取测试结果