同一项目、不同版本之间源码的阅读

上一篇我们讲了如何通过关联延伸阅读梳理项目之间的关系。

本篇是《如何高效阅读源码》专题的第十三篇,来聊一聊如何阅读项目的不同版本。

阅读不同的版本源码的目的有两个:

  • 一个比较火的开源项目,往往经历了较长时间的开发周期,较多的版本迭代。新版本往往比老版本功能更多更完善,在了解了老版本的逻辑后,对于变化不是太大的版本,我们可以通过阅读差异代码来较快的理解新版本的代码逻辑。

  • 而对于逻辑差异较大的版本,我们可以对比差异,理解为什么会有这样的差异,是什么原因导致了这样的差异,继而 更好的理解新版本的项目

对于JUnit来说,JUnit4和JUnit3之间的差异很大,我们通过阅读JUnit4和JUnit3之间的差异来理解版本之间的差异,以及理解为什么会有这些差异。

最新的JUnit5的变化更大,拆分成了三个大模块。鉴于篇幅,这里不做介绍。
JUnit4的流程已经梳理过了,我们根据前面的步骤来快速的梳理JUnit3,并比较一下两者的差异。

JUnit3源码快速梳理

找出核心模块

在专栏第六篇文章,我们梳理出了下图的核心模块。

同一项目、不同版本之间源码的阅读

从依赖关系,我们可以知道junit.framework是最核心的模块,而junit.runner次之,junit.textui则是最外围的模块,从名字也可以知道textui是用于做展示用的。

找出核心模型

通过对framework的梳理,我们可以梳理出下图的核心模型

同一项目、不同版本之间源码的阅读

梳理核心流程(同时提出问题)

我们从Test接口开始梳理,Test接口的代码如下:同一项目、不同版本之间源码的阅读

可以一眼就看出核心方法是run方法,它是用来执行测试用例的。传入的TestResult用于收集测试的结果。在子类TestCase中的实现是直接委托给TestResult的run方法来执行

同一项目、不同版本之间源码的阅读

我们接下来看TestResult类的run方法:

同一项目、不同版本之间源码的阅读

核心的方法是构建了一个Protectable的匿名类,然后去调用runProtected方法去执行。
为什么要构建一个Protectable的匿名类来执行呢?直接执行不好吗?
我们继续深入runProtected方法:

同一项目、不同版本之间源码的阅读

这里就是执行前面匿名Protectable类的protect方法,如果报错则进行错误信息的记录。这里使用try-catch很好理解,因为有多个测试要执行,不能因为一个测试用例失败就导致后面的测试用例不执行了。

protect方法最终执行的是test的runBare方法:

同一项目、不同版本之间源码的阅读

这里我们就可以看到测试执行的流程了:

  • 首先执行setUp()方法,就是我们在TestCase中编写的setUp方法

  • runTest执行测试方法

  • 最终执行tearDown方法,做清理工作

  • 如果有异常就抛出异常。这里抛出的异常就会被前面的runProtected方法catch到,记录到TestResult中

很明显,这里是个模板方法模式,这里定义了测试的整体流程,而我们编写的TestCase就是具体的测试逻辑实现。

模板方法模式:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Template Method使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

最后就是runTest方法:

同一项目、不同版本之间源码的阅读

这里的代码看起来很多,逻辑实际很简单,就是使用反射来执行方法!

画图加深理解

同一项目、不同版本之间源码的阅读

 

 同一项目、不同版本之间源码的阅读

阅读扩展模块,补充流程

在JUnit4中向下兼容了JUnit3,是如何兼容的呢?从前面我们梳理的JUnit4的源码,再结合这里JUnit3的流程,我们可以知道,兼容的方式是通过Runner来处理的。

前面我们知道了AllDefaultPossibilitiesBuilder的runnerForClass方法构建了5个默认的builder.

同一项目、不同版本之间源码的阅读

其中有一个junit3Builder,这里就是用于构建执行JUnit3的Runner的。有兴趣的可自行梳理。

理解核心流程设计

我们回答上面提出的问题:为什么要用一个Protectable构建一个匿名类来执行呢?直接执行不好吗?
实际上,这主要是为了规避代码重复!
查看源码,我们可以看到TestSetup类中的run方法,也实例化了一个Protectable对象,继而进行调用。

同一项目、不同版本之间源码的阅读

在这里,Protectable中封装了不同的执行逻辑,然后传递给runProtected方法来执行。
这和JUnit4里的Statement的作用是否类似?也是用的命令模式。

JUnit3与JUnit4的差异

两者的一个很明显的差异就是技术实现上的差异,JUnit4使用了注解,而JUnit3没有。如果你了解JUnit的发展史,你就能很容易理解。JUnit4的版本是在jdk5发布之后,而jdk5的一个重要特性就是引入了注解。

这也是为什么在专栏第二篇里提出「要了解版本技术背景」的原因
技术实现上的差异导致了代码结构的差异,可以对比一下JUnit3和JUnit4的抽象模型:

同一项目、不同版本之间源码的阅读

 

 同一项目、不同版本之间源码的阅读

虽然JUnit3的核心类相对更少,但实际上JUnit4的结构更清晰。比如:JUnit3的核心类既承担了建模的作用,又承担了执行流程的职责,执行流程在Test和TestResult之间绕了很多次,代码看起来也比较累;而JUnit4中测试执行过程独立到Runner中,TestClass等类只负责建模。这更加的符合单一职责原则。

同时JUnit4为了兼容JUnit3,实际使用的是一个策略模式来构建不同的Runner来执行对应的测试模型。

策略模式:定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。

总结

本文首先通过前面梳理的阅读源码的流程,快速的对JUnit3进行了梳理,接着比较了JUnit3和JUnit4之间的差异。总体而言,JUnit4比JUnit3结构更清晰。

至此,我们源码阅读的完整步骤已经全部讲解完。下一篇是本专栏的最后一篇,对源码阅读做一个总结。

发表评论

相关文章