单元测试(8)- juni4 源码解析

提个问题:Java程序的入口是main方法,而JUnit类中没有main方法,那么JUnit是如何运行的呢?

假如有以下JUnit例子:

一、在Eclipse中执行JUnit

在Eclipse中右键->Run As->JUnit Test,即可执行这个测试。如果在testAdd方法中打上断点(即第6行),我们来看下程序调用的栈:

image

可以看到,这个测试的JVM进程由RemoteTestRunner.main()方法启动,这个类是来自Eclipse的JUnit插件的,真正进入到JUnit的代码是BlockJUnit4ClassRunner.run(RunNotifier)。那么,Eclipse是如何开始调用BlockJUnit4ClassRunner的呢?

image

原来,在执行JUnit测试用例之前,Eclipse将会为这个测试类找到合适的Runner。看这段代码,这里给出了5种RunnerBuilder,它们会被按顺序依次遍历,找到一个合适的Runner后即停止:

  1. IgnoreBuilder。检查被测类是否含有@Ignore注解,如果有,则初始化一个IgnoredClassRunner,否则返回null。
  2. AnnotatedBuilder。检查被测类是否含有@RunWith注解,如果有,则用该注解的value初始化一个Runner,否则返回null。
  3. SuiteMethodBuilder。检查被测类是否含有一个叫“suite”的方法,如果有,则初始化一个SuiteMethod(这是JUnit3.8中使用的Runner),否则返回null。
  4. JUnit3Builder。检查被测类是否是TestCase的子类,如果是,则初始化一个JUnit38ClassRunner,否则返回null。
  5. JUnit4Builder。没有检查条件。将初始化一个BlockJUnint4ClassRunner。这也是JUnit4默认的Runner

经过这5步,必然会找到一个Runner,我们这个了例子就会返回BlockJUnit4ClassRunner。接下来,我们看看这个JUnit4ClassRunner中发生了什么。

在上面已经提到,程序会在执行时会调用到BlockJUnit4ClassRunner.run方法,下面是在其父类ParentRunner类中的实现:

这里主要调用classBlock(RunNotifier)方法(这是一个处理测试类的函数)创建了一个Statement,然后调用Statement.evaluate()完成测试。接下来看看classBlock方法做了些什么:

主要的逻辑是调用了childrenInvoker生成了一条Statement,而这个Statement的evaluate方法是调用一个runChildren方法(这个方法将在BlockJUnit4ClassRunner.run方法中调用statement.evalueate()方法是被回调):

这个runChildren()会遍历这个测试类中的所有测试用例,并为每个用例进一步调用ParentRunner中的抽象方法

在classBlock中还可以看到,如果测试类中的不是所有用例都用例(带有@Test注解)都被忽略(带有@Ignore注解)的时候,会把@BeforeClass,@AfterClass,@ClassRule这几个注解对应的信息添加到Statement中。

接下来看BlockJUnit4ClassRunner的runChild的实现:

runChild表示运行一个带有@Test注解的测试用例。可以看到,如果这个用例标注了@Ignore(isIgnored方法即是探测该用例是否含有@Ignore注解),则不会执行。否则会调用runLeaf方法来执行,这里面就是调用了Statement的evaluate方法:

再看看这个methodBlock方法(和前文classBlock类似,这是一个处理测试方法的函数)都做了些什么:

这里面通过使用多次组合的方式,形成了一个复杂的Statement对象:

  1. 先调用methodInvoker创建了一个InvokeMethod(Statement的子类)对象
  2. 如果该用例用@Test(expected="xxx")标注,则用statement组装并返回一个ExpectException(Statement的子类)对象
  3. 如果该用例用@Test(timeout="xxx")标注,则用statement组装并返回一个FailOnTimeout(Statement的子类)对象
  4. 如果该测试类中有方法用@Before标记,则用statement组装并返回一个RunBefores(Statement的子类)对象
  5. 如果该测试类中有方法用@After标记,则用statement组装并返回一个RunAfters(Statement的子类)对象
  6. 如果该用例上有@Rule,则用statement组装并返回一个RunRules(Statement的子类)对象

注意,Statement是一个抽象类,这里提到的InvokeMethod,ExpectException,FailOnTimeout,RunBefores,RunAfters,RunRules这些对象都是Statement类的子类,都实现了evaluate方法,在不同对象的evaluate方法中除了调用子对象的evaluate方法,还有其他的事情。

前文提到,runLeaf方法会调用这个这个statement对象的evaluate方法,这样这个复杂的statement对象会被依次当作各种对象处理并将调用相应对象的evaluate方法,这种链式调用将在合适的时机完成测试用例的所有步骤。

总结

最关键的地方是BlockJUnit4ClassRunner.run()方法:

第一句用于生成一个Statement对象,这个对象里面封装了类级别(classBlock)和方法级别(methodBlock)的所有注解相关的信息。

第二句statement.evaluate()方法将触发整个链式调用,将会按照组合的先后顺序倒序执行,比如,生成Statement的顺序为:

那么在执行evaluate方法的时候:

  1. 会先将Statement看做是RunRules并调用其evaluate,
  2. 这会把Statement看做是RunAfters并调用其evaluate,
  3. 这又会把Statement看做是RunBefores并会调用其evalueate,
  4. 这样又会调用childrenInvoker返回的Statement的evaluate,而这是一个回调函数,即会调用runChildren(notifier);
  5. 遍历所有的用例并为每个用例调用runChild(each, notifier);
  6. runChild()方法会调用runLeaf()方法(里面也同样是倒叙执行的过程),以完成一个用例的执行。

有同学可能会问了,以上都是使用Eclipse自带的插件来执行JUnit的,如果不用Eclipse,如何执行呢?手动执行的入口又在哪里呢?

二、在命令行中执行JUnit

接下来,我们用命令行来执行这个用例。不过,在执行前需要先确保JUnit4相关的jar包(主要是junit和hamcrest-core这两个jar包)在classpath中。Win10中可以对classpath环境变量如下配置:

image

之后打开命令行,可以先用以下方式检查配置的classpath是否生效:

image

这里我直接复制这个TestA.java到F根目录并编译它:

image

注意执行测试的时候,需要使用如下命令:

这里的org.junit.runner.JUnitCore类来自junit.jar包,它里面的main方法就是整个程序的入口了,一起看看源代码:

args参数即为我们指定的要执行的测试类名字,这里为TestA。可以看见,main方法中创建了一个JUnitCore实例,然后调用runMain()方法:

runMain方法第一句输出了JUnit的版本信息,这和上图的执行结果一致。接下来调用了run()方法:

可见,最终还是调用了Runner.run方法。由于这个该方法是一个抽象函数,那么,程序是如何知道最终应该调用默认的BlockJUnit4ClassRunner的run方法呢?

答案就在这个jUnitCommandLineParseResult.createRequest方法中:

这里面又调用了Request.classes()方法来创建一个request:

而Computer.getSuite的代码如下:

image

这样最终就会调用到builder.runnerForClasses(testClass)。由上方可知,这个builder是AllDefaultPossibilitiesBuilder,而builder.runnerForClasses(testClass)方法正是之前分析Eclipse中执行JUnit过程时分析到的获取BlockJUnit4ClassRunner的过程(见图2)。

就是说,从这里开始就和使用Eclipse来执行JUnit的调用过程BlockJUnit4ClassRunner.run吻合了。

原文链接: https://my.oschina.net/itblog/blog/1550931

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,869评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,716评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,223评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,047评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,089评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,839评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,516评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,410评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,920评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,052评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,179评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,868评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,522评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,070评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,186评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,487评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,162评论 2 356

推荐阅读更多精彩内容