先介绍下这篇博文的由来,以前已经对JUnit的使用经行了深刻的介绍和演示(参考JUnit学习(一),JUnit学习(二)),其中的部分功能是经过分析JUnit源代码找到的。得益于这个过程有幸完整的拜读了JUnit的源码十分赞叹做者代码的精美,一直计划着把源码的分析也写出来。突发奇想决定从设计模式入手赏析JUnit的流程和模式的应用,但愿由此能写出一篇耐读好看的文章。因而又花了些时日重读《设计模式》以期可以顺畅的把二者结合在一块儿,因为我的水平有限不免出现错误、疏漏,还请各位高手多多指出、讨论。 java
首先,介绍下JUnit的测试用例运行会通过哪些过程,这里提及来有些抽象会让人比较迷惑,在看了后面章节的内容以后就比较清晰了: 算法
首先先介绍下JUnit中的模型类(Model),在JUnit模型类能够划分为三个范围: 设计模式
言归正传,下面讨论设计模式和JUnit的源码:
工厂方法模式、职责链:用例启动,Client在建立Request后会调用RunnerBuilder(工厂方法的抽象类)来建立Runner,默认的实现是AllDefaultPosibilitiesBuilder,根据不一样的测试类定义(@RunWith的信息)返回Runner。AllDefaultPosibilitiesBuilder使用职责链模式来建立Runner,部分代码以下。代码A是AllDefaultPosibilitiesBuilder的主要构造逻辑构造了一个【IgnoreBuilder->AnnotatedBuilder->SuitMethodBuilder->JUnit3Builder->JUnit4Builder】的职责链,构造Runner的过程当中有且只有一个handler会响应请求。代码B是Junit4Builder类实现会返回一个BlockJUnit4ClassRunner对象,这个是JUnit4的默认Runner。 app
public Runner runnerForClass(Class<?> testClass) throws Throwable { List<RunnerBuilder> builders = Arrays.asList( ignoredBuilder(), annotatedBuilder(), suiteMethodBuilder(), junit3Builder(), junit4Builder()); for (RunnerBuilder each : builders) { Runner runner = each.safeRunnerForClass(testClass); if (runner != null) { return runner; } } return null; }
public class JUnit4Builder extends RunnerBuilder { @Override public Runner runnerForClass(Class<?> testClass) throws Throwable { return new BlockJUnit4ClassRunner(testClass); } }
代码B JUnit4Builder实现 框架
组合模式:将具有树形结构的数据抽象出公共的接口,在遍历的过程当中应用一样的处理方式。这个模式在Runner中的应用不是很明显,扣进来略有牵强。Runner是分层次的,父层包括@BeforeClass、@AfterClass、@ClassRule注解修饰的方法或变量,它们在测试执行前或执行后执行一次。儿子层是@Before、@After、@Rule修饰的方法或变量它们在每一个测试方法执行先后执行。当编写的用例使用Suit来运行时则是三层结构,上面的父子结构中间插入了一层childrenRunners,也就是一个Suilt中每一个测试类都会生成一个Runner,调用顺序变成了Runner.run()>childRunner.run()<即遍历childrenRunners>->testMethod()。ParentRunner中将变化的部分封装为runChild()方法交给子类实现,达到了遍历过程使用一样处理方式的目的——ParentRunner.this.runChild(each,notifier)。 eclipse
public void run(final RunNotifier notifier) { EachTestNotifier testNotifier = new EachTestNotifier(notifier, getDescription()); try { Statement statement = classBlock(notifier); statement.evaluate(); } catch (AssumptionViolatedException e) { testNotifier.fireTestIgnored(); } catch (StoppedByUserException e) { throw e; } catch (Throwable e) { testNotifier.addFailure(e); } } private void runChildren(final RunNotifier notifier) { for (final T each : getFilteredChildren()) { fScheduler.schedule(new Runnable() { public void run() { ParentRunner.this.runChild(each, notifier); } }); } fScheduler.finished(); }
代码C ParentRunner的组合模式应用 maven
模板方法模式:模板方法的目的是抽取公共部分封装变化,在父类中会包含公共流程的代码,将变化的部分封装为抽象方法由子类实现(就像模板同样框架式定好的,你去填写你须要的内容就好了)。JUnit的默认Runner——BlockJUnit4ClassRunner继承自ParentRunner,ParentRunner类定义了Statement的构造和执行流程,而如何执行儿子层的runChild方法时交给子类实现的,在BlockJUnit4ClassRunner中就是去构造和运行TestMethod,而另外一个子类Suit中则是执行子层次的runner.run。 ide
观察者模式:Runner在执行TestCase过程当中的各个阶段都会通知RunNotifier,其中RunNotifier负责listener的管理者角色,支持添加和删除监听者,提供了监听JUnit运行的方法:如用例开始、完成、失败、成功、忽略等。代码D截取自RunNotifier。SafeNotifier是RunNotifier的内部类,抽取了公共逻辑——遍历注册的listener,调用notifyListener方法。fireTestRunStarted()方法是RunNotifier众多fireXXX()方法的一个,它在方法里构造SafeNotifier的匿名类实现notifyListener()方法。private abstract class SafeNotifier { private final List<RunListener> fCurrentListeners; SafeNotifier() { this(fListeners); } SafeNotifier(List<RunListener> currentListeners) { fCurrentListeners = currentListeners; } void run() { synchronized (fListeners) { List<RunListener> safeListeners = new ArrayList<RunListener>(); List<Failure> failures = new ArrayList<Failure>(); for (Iterator<RunListener> all = fCurrentListeners.iterator(); all .hasNext(); ) { try { RunListener listener = all.next(); notifyListener(listener); safeListeners.add(listener); } catch (Exception e) { failures.add(new Failure(Description.TEST_MECHANISM, e)); } } fireTestFailures(safeListeners, failures); } } abstract protected void notifyListener(RunListener each) throws Exception; } public void fireTestRunStarted(final Description description) { new SafeNotifier() { @Override protected void notifyListener(RunListener each) throws Exception { each.testRunStarted(description); } ; }.run(); }
装饰模式:保持对象原有的接口不改变而透明的增长对象的行为,看起来像是在原有对象外面包装了一层(或多层)行为——虽然对象仍是原来的类型可是行为逐渐丰富起来。 以前一直在强调Statement描述了测试类的执行细节,究竟是如何描述的呢?代码E展现了Statement的构筑过程,首先是调用childrenInvoker方法构建了Statement的基本行为——执行全部的子测试runChildren(notifier)(非Suit状况下就是TestMethod了,若是是Suit的话则是childrenRunners)。
接着是装饰模式的应用,代码F是withBeforeClasses()的实现——很简单,检查是否使用了@BeforeClasses注解修饰若是存在构造RunBefores对象——RunBefore继承自Statement。代码H中的evaluate()方法能够发现新生成的Statement在执行runChildren(fNext.evaluate())以前遍历全部使用@BeforeClasses注解修饰的方法并执行。产生的效果即便用@BeforeClasses修饰的方法会在全部用例运行前执行且只执行一次。后面的withAfterClasses、withClassRules方法原理同样都使用了装饰模式,再也不赘述。 函数
protected Statement classBlock(final RunNotifier notifier) { Statement statement = childrenInvoker(notifier); statement = withBeforeClasses(statement); statement = withAfterClasses(statement); statement = withClassRules(statement); return statement; } protected Statement childrenInvoker(final RunNotifier notifier) { return new Statement() { @Override public void evaluate() { runChildren(notifier); } }; }
protected Statement withBeforeClasses(Statement statement) { List<FrameworkMethod> befores = fTestClass .getAnnotatedMethods(BeforeClass.class); return befores.isEmpty() ? statement : new RunBefores(statement, befores, null); }
public class RunBefores extends Statement { private final Statement fNext; private final Object fTarget; private final List<FrameworkMethod> fBefores; public RunBefores(Statement next, List<FrameworkMethod> befores, Object target) { fNext = next; fBefores = befores; fTarget = target; } @Override public void evaluate() throws Throwable { for (FrameworkMethod before : fBefores) { before.invokeExplosively(fTarget); } fNext.evaluate(); } }
策略模式:针对相同的行为在不一样场景下算法不一样的状况,抽象出接口类,在子类中实现不一样的算法并提供算法执行必须Context信息。JUnit中提供了Timeout、ExpectedException、ExternalResource等一系列的TestRule用于丰富测试用例的行为,这些TestRule的都是经过修饰Statement实现的。
修饰Statement的代码在withRules()方法中实现,使用了策略模式。代码I描述了JUnit是如何处理@Rule标签的,withRules方法获取到测试类中全部的@Rule修饰的变量,分别调用withMethodRules和withTestRules方法,前者是为了兼容JUnit3版本的Rule这里忽略,后者withTestRules的逻辑很简单首先查看是否使用了@Rule,如存在就交给RunRules类处理。代码J是RunRules的实现,在构造函数中处理了修饰Statement的逻辑(applyAll方法)——抽象接口是TestRule,根据不一样的场景(即便用@Rule修饰的不一样的TestRule的实现)选择不一样的策略(TestRule的具体实现),而Context信息就是入参(result:Statement, description:Description)。 学习
private Statement withRules(FrameworkMethod method, Object target, Statement statement) { List<TestRule> testRules = getTestRules(target); Statement result = statement; result = withMethodRules(method, testRules, target, result); result = withTestRules(method, testRules, result); return result; } private Statement withTestRules(FrameworkMethod method, List<TestRule> testRules, Statement statement) { return testRules.isEmpty() ? statement : new RunRules(statement, testRules, describeChild(method)); }
public class RunRules extends Statement { private final Statement statement; public RunRules(Statement base, Iterable<TestRule> rules, Description description) { statement = applyAll(base, rules, description); } @Override public void evaluate() throws Throwable { statement.evaluate(); } private static Statement applyAll(Statement result, Iterable<TestRule> rules, Description description) { for (TestRule each : rules) { result = each.apply(result, description); } return result; } }
public class Timeout implements TestRule { private final int fMillis; /** * @param millis the millisecond timeout */ public Timeout(int millis) { fMillis = millis; } public Statement apply(Statement base, Description description) { return new FailOnTimeout(base, fMillis); } }
public class TestClass { private final Class<?> fClass; private Map<Class<?>, List<FrameworkMethod>> fMethodsForAnnotations = new HashMap<Class<?>, List<FrameworkMethod>>(); private Map<Class<?>, List<FrameworkField>> fFieldsForAnnotations = new HashMap<Class<?>, List<FrameworkField>>(); /** * Creates a {@code TestClass} wrapping {@code klass}. Each time this * constructor executes, the class is scanned for annotations, which can be * an expensive process (we hope in future JDK's it will not be.) Therefore, * try to share instances of {@code TestClass} where possible. */ public TestClass(Class<?> klass) { fClass = klass; if (klass != null && klass.getConstructors().length > 1) { throw new IllegalArgumentException( "Test class can only have one constructor"); } for (Class<?> eachClass : getSuperClasses(fClass)) { for (Method eachMethod : MethodSorter.getDeclaredMethods(eachClass)) { addToAnnotationLists(new FrameworkMethod(eachMethod), fMethodsForAnnotations); } for (Field eachField : eachClass.getDeclaredFields()) { addToAnnotationLists(new FrameworkField(eachField), fFieldsForAnnotations); } } } …… }
读JUnit代码时确实很是赞叹其合理的封装和灵活的设计,本身虽然也写了几年代码可是在JUnit的源码中收获不少。因为对源码的钻研深度以及设计模式的领会不够深刻,文中有不少牵强和错误的地方欢迎你们讨论指正。最喜欢的是JUnit对装饰模式和职责链的应用,在看到AllDefaultPossiblitiesBuilder中对职责链的应用还以为设计比较合理,等到看到Statement的建立和组装就感慨设计的精湛了,不管是基本的调用测试方法的逻辑仍是@Before、@After等以及实现自TestRule的逻辑一并融入到Statement的构造中,又不会牵扯出太多的耦合。总之不管是设计模式仍是设计思想,归根结底就是抽取公共部分,封装变化,作到灵活、解耦。最后说明这篇文章根据的源代码是JUnit4.11的,maven坐标以下,在JUnit的其它版本中源码差异比较大没有研究过。
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> </dependency>