转载自:
http://www.blogjava.net/DLevin/archive/2012/05/11/377954.html
深入JUnit源码之Statement
看JUnit源码最大的收获就是看到这个Statement的设计,它也是我看到过的所有源码中最喜欢的设计之一。JUnit中Runner的运行过程就是Statement链的运行过程,Statement是对一个单元运行的封装,每个Statement都只是执行它本身所表达的逻辑,而将其他逻辑交给下一个Statement处理,而且基本上的Statement都存在对下一个节点的引用,从而由此构成一条Statement的链,从设计模式的角度上来看,这是一个职责连模式(Chain Of Responsibility Pattern)。JUnit中对@BeforeClass、@AfterClass、@Before、@After、@ClassRule、@Rule等逻辑就是通过Statement来实现的。首先来看一下Statement的类结构。
Statement的类结构还是比较简单的,首先Statement是所有类的父类,它只定义了一个抽象的evaluate()方法,由其他子类实现该方法;而且除了Fail和InvokeMethod类,其他类都有一个对Statement本身的引用。其实从实现上,每个Statement也是比较简单的,这个接下来就可以看到了。每个Statement都只实现它自己的逻辑,而将其他逻辑代理给另一个Statement执行,这样可以在编写每个Statement的时候只关注自己的逻辑,从而保持Statement本身的简单,并且易于扩展,当一条Statement执行完后,整个逻辑也就执行完了。不过Statement这条链也不是凭空产生的,它也是要根据一定的逻辑构造起来的,关于Statement链的构造在JUnit中由Runner负责,为了保持本文的完整性,本文会首先会讲解上述几个Statement的源码,同时简单回顾Statement链的构造过程,最后将通过一个简单的例子,将Statement的执行过程用序列图的方式表达出来,以更加清晰的表达Statement的执行过程。不过本文不会详细介绍Rules相关的代码,这部分的代码将会在下一节详细介绍。
RunBefores和RunAfters
这两个Statement是针对JUnit中@BeforeClass、@Before的实现的,其中@BeforeClass是在测试类运行时,所有测试方法运行之前运行,并且对每个测试类只运行一次,这个注解修饰的方法必须是静态的(在Runner一节中有谈过它为什么要被设计成一定是要静态方法,因为在运行每个测试方法是,测试类都会从新初始化一遍,如果不是静态类,它只运行一次的话,它运行的结果无法保存下来);@Before是在每个测试方法运行之前都要运行。
在Statement的设计中,@BeforeClass注解的方法抽象成一个Statement叫RunBefores,而测试类中其他要运行的测试方法的运行过程是另一个Statement叫next,在RunBefores中调用完所有这些方法,而将其他逻辑交给next; @Before注解的方法也是一样的逻辑,它把接下来的测试方法看成是一个Statement叫next,它调用完所有@Before注解的方法后,将接下来的事情交给next,因而他们共享RunBefores的Statement,唯一不同的是@BeforeClass的RunBefores可以直接调用测试类中的方法,因为他们是静态的,而@Before的RunBefores需要传入测试类的实例。
2 private final Statement fNext;
3 private final Object fTarget;
4 private final List<FrameworkMethod> fBefores;
5 public RunBefores(Statement next, List<FrameworkMethod> befores, Object target) {
6 fNext= next;
7 fBefores= befores;
8 fTarget= target;
9 }
10 @Override
11 public void evaluate() throws Throwable {
12 for (FrameworkMethod before : fBefores)
13 before.invokeExplosively(fTarget);
14 fNext.evaluate();
15 }
16 }
从源码中可以看到,构造RunBefores时传入下一个Statement、所有@BeforeClass或@Before注解的方法以及测试类的实例,对@BeforeClass来说,传入null即可。在运行evaluate()方法时,它依次调用@BeforeClass或@Before注解的方法,后将接下来的逻辑交给next。从这段逻辑也可以看出如果有一个@BeforeClass或@Before注解的方法抛异常,接下来的这些方法就都不会执行了,包括测试方法。不过此时@AfterClass或@After注解的方法还会执行,这个在下一小节中即可知道。
关于RunBefores的构造要,其实最重要的是要关注它的next Statement是什么,对于@BeforeClass对应的RunBefores来说,它的next Statement那些所有的测试方法运行而组成的Statement,而对于@Before对应的RunBefores来说,它的next Statement是测试方法的Statement:
2 Statement statement= childrenInvoker(notifier);
3 statement= withBeforeClasses(statement);
4
5 }
6 protected Statement withBeforeClasses(Statement statement) {
7 List<FrameworkMethod> befores= fTestClass
8 .getAnnotatedMethods(BeforeClass.class);
9 return befores.isEmpty() ? statement :
10 new RunBefores(statement, befores, null);
11 }
12 protected Statement methodBlock(FrameworkMethod method) {
13
14 Statement statement= methodInvoker(method, test);
15
16 statement= withBefores(method, test, statement);
17
18 }
19 protected Statement withBefores(FrameworkMethod method, Object target,
20 Statement statement) {
21 List<FrameworkMethod> befores= getTestClass().getAnnotatedMethods(
22 Before.class);
23 return befores.isEmpty() ? statement : new RunBefores(statement,
24 befores, target);
25 }
@AfterClass和@After
这两个Statement是针对JUnit中@AfterClass、@After的实现的,其中@AfterClass是在测试类运行时,所有测试方法结束之后运行,不管之前的方法是否有抛异常,并且对每个测试类只运行一次,这个注解修饰的方法必须是静态的;@After是在每个测试方法运行结束后都要运行的,不管测试方法是否测试失败。
在Statement的设计中,@AfterClass注解的方法抽象成一个Statement叫Afters,而测试类中之前要运行的过程是另一个Statement叫next(其实这个叫before更好一些),在RunAfters中等所有之前的运行过程调用完后,再调用@AfterClass注解的方法; @After注解的方法也是一样的逻辑,它把之前的测试方法包括@Before注解的方法看成是一个Statement叫next(before?),它等测试方法或@Before注解的方法调用完后,调用@After注解的方法,因而他们共享RunAfters的Statement,唯一不同的是@AfterClass的RunAfters可以直接调用测试类中的方法,因为他们是静态的,而@After的RunAfters需要传入测试类的实例。
2 private final Statement fNext;
3 private final Object fTarget;
4 private final List<FrameworkMethod> fAfters;
5 public RunAfters(Statement next, List<FrameworkMethod> afters, Object target) {
6 fNext= next;
7 fAfters= afters;
8 fTarget= target;
9 }
10 @Override
11 public void evaluate() throws Throwable {
12 List<Throwable> errors = new ArrayList<Throwable>();
13 try {
14 fNext.evaluate();
15 } catch (Throwable e) {
16 errors.add(e);
17 } finally {
18 for (FrameworkMethod each : fAfters)
19 try {
20 each.invokeExplosively(fTarget);
21 } catch (Throwable e) {
22 errors.add(e);
23 }
24 }
25 MultipleFailureException.assertEmpty(errors);
26 }
27 }
从源码中可以看到,构造RunAfters时传入下一个Statement、所有@AfterClass或@After注解的方法以及测试类的实例,对@AfterClass来说,传入null即可。在运行evaluate()方法时,它会等之前的Statement执行结束后,再依次调用@AfterClass或@After注解的方法。从这段逻辑也可以看出无论之前Statement执行是否抛异常,@AfterClass或@After注解的方法都是会被执行的,为了避免在执行@AfterClass或@After注解的方法抛出的异常覆盖之前在运行@BeforeClass、@Before或@Test注解方法抛出的异常,这里所有的异常都会触发一次testFailure的事件,这个实现可以查看Runner小节的EachTestNotifier类的实现。
对于RunAfters的构造,可能要注意的一点是传入RunAfters的Statement是RunBefores的实例,这个其实还是好理解的,因为RunAfters是在传入的Statement运行结束后运行,而RunBefores又是要在测试方法之前运行的,因而需要将RunAfters放在Statement链的最头端,而后是RunAfters,最后才是测试方法调用的Statement(InvokeMethod)。
2
3 statement= withBeforeClasses(statement);
4 statement= withBeforeClasses(statement);
5
6 }
7 protected Statement withAfterClasses(Statement statement) {
8 List<FrameworkMethod> afters= fTestClass
9 .getAnnotatedMethods(AfterClass.class);
10 return afters.isEmpty() ? statement :
11 new RunAfters(statement, afters, null);
12 }
13 protected Statement methodBlock(FrameworkMethod method) {
14
15 statement= withBefores(method, test, statement);
16 statement= withAfters(method, test, statement);
17
18 }
19 protected Statement withAfters(FrameworkMethod method, Object target,
20 Statement statement) {
21 List<FrameworkMethod> afters= getTestClass().getAnnotatedMethods(
22 After.class);
23 return afters.isEmpty() ? statement : new RunAfters(statement, afters,
24 target);
25 }
InvokeMethod、ExpectedException和FailOnTimeout
之所有要把这三个Statement放在一起是因为他们都是和@Test注解相关的:
2 @Target({ElementType.METHOD})
3 public @interface Test {
4 Class<? extends Throwable> expected() default None.class;
5 long timeout() default 0L;
6 }
@Test注解定义了两个成员:expected指定当前测试方法如果抛出指定的异常则表明测试成功;而timeout指定当前测试方法如果超出指定的时间(以毫秒为单位),则测试失败。在Statement设计中,这些逻辑都抽象成了一个Statement。而@Test注解的方法则被认为是真正要运行的测试方法,它的执行过程也被抽象成了一个Statement。
@Test注解的方法抽象出的Statement命名为InvokeMethod,它是一个非常简单的Statement:
2 private final FrameworkMethod fTestMethod;
3 private Object fTarget;
4 public InvokeMethod(FrameworkMethod testMethod, Object target) {
5 fTestMethod= testMethod;
6 fTarget= target;
7 }
8 @Override
9 public void evaluate() throws Throwable {
10 fTestMethod.invokeExplosively(fTarget);
11 }
12 }
使用一个方法实例和测试类的实例构造InvokeMethod,在运行时直接调用该方法。并且InvokeMethod并没有对其他Statement的引用,因而它是Statement链上的最后一个节点。
2
3 Statement statement= methodInvoker(method, test);
4 statement= possiblyExpectingExceptions(method, test, statement);
5 statement= withPotentialTimeout(method, test, statement);
6
7 }
8 protected Statement methodInvoker(FrameworkMethod method, Object test) {
9 return new InvokeMethod(method, test);
10 }
ExpectException用于处理当在@Test注解中定义了expected字段时,该注解所在的方法是否在运行过程中真的抛出了指定的异常,如果没有,则表明测试失败,因而它需要该测试方法对应的Statement(InvokeMethod)的引用:
2 private Statement fNext;
3 private final Class<? extends Throwable> fExpected;
4 public ExpectException(Statement next, Class<? extends Throwable> expected) {
5 fNext= next;
6 fExpected= expected;
7 }
8 @Override
9 public void evaluate() throws Exception {
10 boolean complete = false;
11 try {
12 fNext.evaluate();
13 complete = true;
14 } catch (AssumptionViolatedException e) {
15 throw e;
16 } catch (Throwable e) {
17 if (!fExpected.isAssignableFrom(e.getClass())) {
18 String message= "Unexpected exception, expected<"
19 + fExpected.getName() + "> but was<"
20 + e.getClass().getName() + ">";
21 throw new Exception(message, e);
22 }
23 }
24 if (complete)
25 throw new AssertionError("Expected exception: "
26 + fExpected.getName());
27 }
28 }
使用InvokeMethod实例和一个expected的Throwable Class实例作为参数构造ExpectException,当InvokeMethod实例执行后,判断其抛出的异常是否为指定的异常或者该测试方法没有抛出异常,在这两种情况下,测试都会失败,因而需要它抛出异常以处理这种情况。
2
3 Statement statement= methodInvoker(method, test);
4 statement= possiblyExpectingExceptions(method, test, statement);
5
6 }
7 protected Statement possiblyExpectingExceptions(FrameworkMethod method,
8 Object test, Statement next) {
9 Test annotation= method.getAnnotation(Test.class);
10 return expectsException(annotation) ? new ExpectException(next,
11 getExpectedException(annotation)) : next;
12 }
FailOnTimeout是在@Test注解中指定了timeout值时,用于控制@Test注解所在方法的执行时间是否超出了timeout的值,若是,则抛出异常,表明测试失败。在JUnit4当前的实现中,它引用的Statement实例是ExpectException(如果expected字段定义了的话)或InvokeMethod。它通过将引用的Statement实例的执行放到另一个线程中,然后通过控制线程的执行时间以控制内部引用的Statement实例的执行时间,如果测试方法因内部抛出异常而没有完成,则抛出内部抛出的异常实例,否则抛出执行时间超时相关的异常。
2 private final Statement fOriginalStatement;
3 private final long fTimeout;
4 public FailOnTimeout(Statement originalStatement, long timeout) {
5 fOriginalStatement= originalStatement;
6 fTimeout= timeout;
7 }
8 @Override
9 public void evaluate() throws Throwable {
10 StatementThread thread= evaluateStatement();
11 if (!thread.fFinished)
12 throwExceptionForUnfinishedThread(thread);
13 }
14 private StatementThread evaluateStatement() throws InterruptedException {
15 StatementThread thread= new StatementThread(fOriginalStatement);
16 thread.start();
17 thread.join(fTimeout);
18 thread.interrupt();
19 return thread;
20 }
21 private void throwExceptionForUnfinishedThread(StatementThread thread)
22 throws Throwable {
23 if (thread.fExceptionThrownByOriginalStatement != null)
24 throw thread.fExceptionThrownByOriginalStatement;
25 else
26 throwTimeoutException(thread);
27 }
28 private void throwTimeoutException(StatementThread thread) throws Exception {
29 Exception exception= new Exception(String.format(
30 "test timed out after %d milliseconds", fTimeout));
31 exception.setStackTrace(thread.getStackTrace());
32 throw exception;
33 }
34 private static class StatementThread extends Thread {
35 private final Statement fStatement;
36 private boolean fFinished= false;
37 private Throwable fExceptionThrownByOriginalStatement= null;
38 public StatementThread(Statement statement) {
39 fStatement= statement;
40 }
41 @Override
42 public void run() {
43 try {
44 fStatement.evaluate();
45 fFinished= true;
46 } catch (InterruptedException e) {
47 //don't log the InterruptedException
48 } catch (Throwable e) {
49 fExceptionThrownByOriginalStatement= e;
50 }
51 }
52 }
53 }
FailOnTimeout的构造过程如同上述的其他Statement类似:
2
3 Statement statement= methodInvoker(method, test);
4 statement= possiblyExpectingExceptions(method, test, statement);
5 statement= withPotentialTimeout(method, test, statement);
6
7 }
8 protected Statement withPotentialTimeout(FrameworkMethod method,
9 Object test, Statement next) {
10 long timeout= getTimeout(method.getAnnotation(Test.class));
11 return timeout > 0 ? new FailOnTimeout(next, timeout) : next;
12 }
Fail与RunRules
Fail这个Statement是在创建测试类出错时构造的Statement,这个设计思想有点类似Null Object模式,即保持编程模型的统一,即使在出错的时候也用一个Statement去封装,这也正是Runner中ErrorReportingRunner的设计思想。
Fail这个Statement很简单,它在运行时重新抛出之前记录的异常,其构造过程也是在创建测试类实例出错时构造:
2 Object test;
3 try {
4 test= new ReflectiveCallable() {
5 @Override
6 protected Object runReflectiveCall() throws Throwable {
7 return createTest();
8 }
9 }.run();
10 } catch (Throwable e) {
11 return new Fail(e);
12 }
13
14 }
15 public class Fail extends Statement {
16 private final Throwable fError;
17 public Fail(Throwable e) {
18 fError= e;
19 }
20 @Override
21 public void evaluate() throws Throwable {
22 throw fError;
23 }
24 }
RunRules这个Statement是对@ClassRule和@Rule运行的封装,它会将定义的所有Rule应用到传入的Statement引用后返回,由于它内部还有一些比较复杂的逻辑,关于Rule将有一个单独的小节讲解。这里主要关注RunRules这个Statement的实现和构造:
2 private final Statement statement;
3 public RunRules(Statement base, Iterable<TestRule> rules, Description description) {
4 statement= applyAll(base, rules, description);
5 }
6 @Override
7 public void evaluate() throws Throwable {
8 statement.evaluate();
9 }
10 private static Statement applyAll(Statement result, Iterable<TestRule> rules,
11 Description description) {
12 for (TestRule each : rules)
13 result= each.apply(result, description);
14 return result;
15 }
16 }
17 protected Statement classBlock(final RunNotifier notifier) {
18
19 statement= withAfterClasses(statement);
20 statement= withClassRules(statement);
21 return statement;
22 }
23 private Statement withClassRules(Statement statement) {
24 List<TestRule> classRules= classRules();
25 return classRules.isEmpty() ? statement :
26 new RunRules(statement, classRules, getDescription());
27 }
28 protected Statement methodBlock(FrameworkMethod method) {
29
30 statement= withRules(method, test, statement);
31 return statement;
32 }
33 private Statement withRules(FrameworkMethod method, Object target,
34 Statement statement) {
35 List<TestRule> testRules= getTestClass().getAnnotatedFieldValues(
36 target, Rule.class, TestRule.class);
37 return testRules.isEmpty() ? statement :
38 new RunRules(statement, testRules, describeChild(method));
39 }
Rule分为@ClassRule和@Rule,@ClassRule是测试类级别的,它对一个测试类只运行一次,而@Rule是测试方法级别的,它在每个测试方法运行时都会运行。RunRules的构造过程中,我们可以发现RunRules才是最外层的Statement,TestRule中要执行的逻辑要么比@BeforeClass、@Before注解的方法要早,要么比@AfterClass、@After注解的方法要迟。
Statement执行序列图
假设在一个测试类中存在多个@BeforeClass、@AfterClass、@Before、@After注解的方法,并且有两个测试方法testMethod1()和testMethod2(),那么他们的运行序列图如下所示(这里没有考虑Rule,对@ClassRule的RunRules,它的链在最前端,而@Rule的RunRules则是在RunAfters的前面,关于Rule将会在下一节中详细讲解):
相关推荐
本篇文章将深入到JUnit的源码中,探讨Runner的实现原理,帮助开发者更好地理解和利用JUnit进行测试。 在JUnit中,Runner的主要职责是负责运行测试案例。默认的`BlockJUnit4ClassRunner`是JUnit4中的基础Runner,它...
本资料包"junit4测试数据库源码"主要涵盖了Junit4的新特性assertThat断言以及如何利用Junit4进行MySQL和Oracle两种主流数据库的增删改查操作的测试。 首先,我们来了解一下Junit4的assertThat断言。在旧版本的Junit...
《探索JUnit4扩展:深入Rule》 JUnit是Java开发者最常用的单元测试框架,它极大地简化了测试代码的编写。在JUnit4中,引入了一个强大的特性——Rule,这使得测试更加灵活且可定制化。本文将深入探讨Rule的概念、...
JUnit4的开源性质使其具备高度的可扩展性,开发者可以通过实现JUnit的扩展接口,如`Statement`和`Runner`,来扩展测试功能或构建自定义测试工具。 通过`junit4-example`项目,我们可以更直观地学习到JUnit4的使用...
标题“statement”可能指的是某个...深入讨论这个话题,我们可以涵盖源码解析、调试技术、代码质量和效率提升方法,以及如何有效地利用各种工具进行协作和项目管理。然而,由于具体信息有限,无法提供更详细的解答。
标签“源码”和“工具”暗示了这个话题可能深入到JUnit的源代码层面,或者探讨如何结合其他工具(如持续集成服务器)使用`@Rule`进行更复杂的测试自动化。在实际开发中,理解`@Rule`的工作原理并结合源码阅读,可以...
标题中的"people_java编程_enjoyz2u_people_源码.zip"表明这...要深入理解这份源码,需要逐一解析每个类和方法,了解它们的作用,以及它们如何协同工作。同时,查阅相关文档、注释和在线资源将有助于更好地理解和学习。
在源码中,可能会看到DataSource的配置和Connection、Statement、ResultSet的使用。 5. **Hibernate与MyBatis**:这些是ORM(Object-Relational Mapping)框架,简化了数据库操作。Hibernate提供了一种强大的对象...
在源码中,可能会看到`Connection`、`Statement`或`PreparedStatement`的使用,以及事务管理的相关代码。 此外,考虑到系统可能涉及到大量并发操作,线程安全性和性能优化是必须考虑的。Java提供了丰富的并发工具类...
本文将深入探讨一个用Java语言编写的图书管理系统,它利用GUI图形化界面为用户提供友好的操作体验。这个系统不仅实现了基础的图书管理功能,还允许开发者根据自身需求进行扩展和定制,具有较高的可扩展性和灵活性。 ...
这些操作通常通过`Statement`或`PreparedStatement`对象完成。 4. **JSP页面**:页面负责展示数据,可以使用EL(Expression Language)和JSTL(JavaServer Pages Standard Tag Library)简化页面中的Java代码。JSP...
学习过程中,应编写单元测试以确保CRUD操作的正确性,可以使用JUnit等测试框架。 10. **日志记录**: 在实际项目中,为便于调试和问题排查,通常会集成日志库如Log4j或SLF4J。 这个"java学习增删改查完整源码"资源...
这里我们将深入探讨这个主题,了解如何使用Java来管理SQL Server数据库的分离,并涉及相关的编程知识点。 1. **Java与SQL Server的连接** - JDBC(Java Database Connectivity)是Java与各种数据库通信的标准API。...
在本项目中,可能会使用PreparedStatement或Statement对象来执行动态或静态SQL,以确保代码安全性和效率。 数据库设计是关键,可能涉及到ER(实体关系)模型的创建,包括表的定义、字段选择、主键和外键的设置,...
《Java源码:阳光酒店管理系统》是一个典型的Java编程实践项目,它涵盖了丰富的IT知识点,尤其在软件工程、数据库设计、用户界面(UI)开发以及Java编程语言的应用方面。以下是这个项目涉及的一些关键技术和概念的...
通过这个Java仓库管理系统,开发者不仅可以学习到如何使用Java语言构建实际应用,还能深入理解数据库操作、MVC模式的应用以及如何优化用户界面。对于初学者来说,这是一个很好的实践项目,而对于有经验的开发者,它...
【ssm项目源码】图书在线管理系统的实现是基于Java技术栈,主要采用了Spring、SpringMVC和MyBatis三个框架的集成,也就是通常所说...通过研究该项目源码,开发者可以深入了解SSM框架的运用以及完整的Web应用开发流程。
1. **Java基础**:Java是该项目的主要编程语言,开发者需要对Java面向对象编程有深入理解,包括类、对象、接口、继承、多态等概念。同时,熟悉Java集合框架(如ArrayList、LinkedList、HashMap等)和异常处理机制也...
10. **测试**:良好的软件工程实践包括单元测试和集成测试,因此源码中可能包含JUnit测试用例,用于验证各个模块的功能正确性。 11. **版本控制**:项目可能使用Git进行版本控制,这样开发者可以协同工作,追踪代码...
9. **单元测试**:源码中可能包含对分页功能的单元测试,通过JUnit或其他测试框架验证分页查询的正确性和性能。 10. **代码注释**:源码中详细的注释对于初学者来说是非常宝贵的资源,可以帮助理解分页查询的实现...