浅解 JUnit 4 第八篇:JUnitCore (上)

背景

我在 浅解 JUnit 4 这个专栏陆续添加了一些文章。现有的文章初步探讨了 Test/Runner/RunnerBuilder 这些核心类的逻辑。但是 JUnit 4 的入口在哪里呢?本文会对这个问题进行探讨(由于 JUnitCore 这个类涉及的内容较多,一篇文章写不完,本文只是 上篇)。

要点

  • org.junit.runner.JUnitCore 类有 main 方法,我们可以在命令行通过 java -jar 的方式来调用这个 main 方法
  • 调用这个 main 方法时,也会有构造 Runner,运行 Runner 这样的逻辑

一些类的全限定类名

文中提到 JUnit 4 中的类,它们的全限定类名一般都比较长,所以文中有时候会用简略的写法(例如将 org.junit.runners.Suite 写成 Suite)。我在这一小节把简略类名和全限定类名的对应关系列出来

简略的类名 全限定类名(Fully Qualified Class Name)
AllDefaultPossibilitiesBuilder org.junit.internal.builders.AllDefaultPossibilitiesBuilder
JUnitCore org.junit.runner.JUnitCore
Request org.junit.runner.Request
RunListener org.junit.runner.notification.RunListener
TextListener org.junit.internal.TextListener

正文

junit.org/junit4/JUnit 4 的网站。这个网站有 Frequently Asked Questions 页面。本文关心其中的 3 个问题(这 3 个问题我在下图用红框标出来了)

  • Where do I download JUnit? (如何下载 JUnit 4?)
  • How do I install JUnit? (如何安装 JUnit 4?)
  • How do I write and run a simple test? (如何写以及运行简单的测试?)

我们逐个来看

问题 1: 如何下载 JUnit 4?

Frequently Asked Questions 页面中提供的是 SourceForge 网站的一个链接(如下图所示)

我觉得直接去 Maven Repository: junit >> junit 下载就行,例如 4.13.2 版本的链接是 Maven Repository: junit >> junit >> 4.13.2

点击下图红框位置,就可以下载 jar

我下载之后,得到的是名为 junit-4.13.2.jar 的文件,如下图所示 👇

由于 JUnit 4 依赖了 hamcrest-core,我们前往 Maven Repository: org.hamcrest >> hamcrest-core >> 1.3 页面把 hamcrest-corejar 包也下载下来(点击下图红框所示的位置就可以下载这个 jar 包)

我下载之后,得到的是名为 hamcrest-core-1.3.jar 的文件,如下图所示 👇

问题 2: 如何安装 JUnit 4?

Frequently Asked Questions 页面提供的方案涉及修改 CLASSPATH (如下图所示)

如果我们是在 maven 项目中使用 JUnit 4,那就需要手动修改 CLASSPATH。如果是在命令行做简单的测试,那么 java 命令有 -cp/-classpath 选项可以调整 classpath (即,类搜索路径)。

问题 3: 如何写以及运行简单的测试?

Frequently Asked Questions 页面提供了一个简单的例子(如下图所示)

一个简单的测试

这个例子的代码可以整理为 ⬇️ (我把 package 语句删掉了)

java 复制代码
import org.junit.*;
import static org.junit.Assert.*;
import java.util.*;

public class SimpleTest {
    @Test
    public void testEmptyCollection() {
        Collection collection = new ArrayList();
        assertTrue(collection.isEmpty());
    }
}

请将以上代码保存为 SimpleTest.java。用以下命令可以编译 SimpleTest.java(问题 1 提到了如何 如何下载 JUnit 4 ,请将下载好的 jar 包移动到当前目录之后再执行下方的命令)

bash 复制代码
javac -cp junit-4.13.2.jar SimpleTest.java

编译成功后,应该可以看到 SimpleTest.class 文件。再执行以下命令就可以运行 SimpleTest 里的单元测试了(我是在 Mac 电脑上执行的,如果您的电脑是其他系统,可能需要调整 -cp 选项里的分隔符)。

bash 复制代码
java -cp .:junit-4.13.2.jar:hamcrest-core-1.3.jar org.junit.runner.JUnitCore SimpleTest

运行结果如下 ⬇️

text 复制代码
JUnit version 4.13.2
.
Time: 0.003

OK (1 test)

我们来看看这些内容是从哪里来的。从 java -cp .:junit-4.13.2.jar:hamcrest-core-1.3.jar org.junit.runner.JUnitCore SimpleTest 这个命令来看,我们会运行 org.junit.runner.JUnitCore 类的 main 方法,而 main 方法的参数是一个数组,这个数组中只有一个元素,即, "SimpleTest" 。我们去看看 org.junit.runner.JUnitCore 这个类 ⬇️

从源代码中确实能找到 main 方法(如上图红框所示)。main 方法中调用了 runMain(...) 方法 ⬇️

我把 runMain(...) 方法的代码复制到下方了 ⬇️

java 复制代码
/**
 * @param system
 * @param args from main()
 */
Result runMain(JUnitSystem system, String... args) {
    system.out().println("JUnit version " + Version.id());

    JUnitCommandLineParseResult jUnitCommandLineParseResult = JUnitCommandLineParseResult.parse(args);

    RunListener listener = new TextListener(system);
    addListener(listener);

    return run(jUnitCommandLineParseResult.createRequest(defaultComputer()));
}

runMain(...) 方法

我觉得可以把 runMain(...) 方法的主要逻辑概括为以下几步

  1. 打印版本号
  2. 解析命令行参数
  3. 添加 TextListener
  4. 构造 Runner 并运行测试

我们逐个来看。

runMain(...) 方法中的第一步: 打印版本号

system.out() 其实就是 System.out。因为 runMain(...) 方法的入参中的 systemorg.junit.internal.RealSystem 类的实例,而 RealSystemout() 方法返回的就是 System.out ⬇️

至于 Version.id() 方法,它的返回值是 "4.13.2" ⬇️

这样,运行结果中的第一行就解释清楚了 ⬇️

runMain(...) 方法中的第二步: 解析命令行参数

我们看一看 org.junit.runner.JUnitCommandLineParseResultparse(String[] args) 方法 ⬇️

  • parse(String[] args) 方法会调用 parseArgs(String[] args) 方法
    • parseArgs(String[] args) 方法会依次调用
      • parseOptions(String... args) 方法
      • parseParameters(String[] args) 方法

其中 parseOptions(String... args) 方法看起来可以处理以下三种选项。

  • --
  • --filter=xxx
  • --filter

不过我没有搜索到关于这三个选项的介绍文档,加之我们的这些小例子里用不到这些选项,所以 parseOptions(String... args) 方法里的逻辑我们就不细看了(可以简单认为这个方法在处理命令行参数里的选项部分)。

至于 parseParameters(String[] args) 方法,它的代码只有几行,我复制到下方了 ⬇️

java 复制代码
void parseParameters(String[] args) {
    for (String arg : args) {
        try {
            classes.add(Classes.getClass(arg));
        } catch (ClassNotFoundException e) {
            parserErrors.add(new IllegalArgumentException("Could not find class [" + arg + "]", e));
        }
    }
}

它会把命令行传过来的各个类名转化为对应的 class 对象。

java -cp .:junit-4.13.2.jar:hamcrest-core-1.3.jar org.junit.runner.JUnitCore SimpleTest 这个命令为例,parseParameters(String[] args) 方法会把 "SimpleTest" 转化为 SimpleTest.class

runMain(...) 方法中的第三步: 添加 TextListener

org.junit.internal.TextListener 继承了 org.junit.runner.notification.RunListener

两者的类图如下 ⬇️

org.junit.runner.notification.RunListenerjavadoc 里可以看到这个类的介绍 ⬇️

其中第一段的内容如下

Register an instance of this class with RunNotifier to be notified of events that occur during a test run. All of the methods in this class are abstract and have no implementation; override one or more methods to receive events.

我献丑来翻译一下 ⬇️

我们需要将 RunListener 的实例注册到 RunNotifier,以便在相关事件发生时,收到对应的通知。RunListener 类中的所有方法的方法体都是空的,请 override 其中的若干个,以收到对应的事件。

TextListener overrideRunListener 中的 4 个方法 ⬇️ 所以在对应的 4 种事件发生时,这 4 个方法会被调用。

java -cp .:junit-4.13.2.jar:hamcrest-core-1.3.jar org.junit.runner.JUnitCore SimpleTest 这个命令的运行结果中的 . 就来自 testStarted(Description description) 方法(如下图所示 ⬇️)

TextListener 中的 testRunFinished(Result result) 方法包含以下三部分

  • 调用 printHeader(long runTime) 方法,展示运行耗时
  • 调用 printFailures(Result result) 方法,展示失败的单元测试的相关信息
  • 调用 printFooter(Result result) 方法,展示单元测试的计数值

printHeader(long runTime) 方法的作用如下图所示 ⬇️

我们这个例子里没有失败的单元测试,所以就不看 printFailures(Result result) 方法了。

printFooter(Result result) 方法的作用如下图所示 ⬇️

runMain(...) 方法中的第四步: 构造 Runner 并运行测试

第四步的逻辑,如果展开来说,会比较多。目前我们只看构造 Runner 的部分(至于如何通过 Runner 来运行测试,后续的文章再来探讨)。构造 Runner 的核心逻辑在 org.junit.runner.Request 类的classes(org.junit.runner.Computer, java.lang.Class<?>...) 方法里。

这个方法的逻辑可以简单概括如下 ⬇️

  • AllDefaultPossibilitiesBuilder 给各个指定的测试类构建对应的 Runner (如果指定了 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 个测试类,则应当生成 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 个 Runner)
  • 将这些 Runner 作为一个 Suitechildren 节点
  • 将这个 Suite 包装成 org.junit.runner.Request

到这里就把 JUnitCore 中的一个入口(即,main 方法)介绍完了。下一篇我们会探讨 JUnitCore 的另一个入口。

其他

画"org.junit.runner.notification.RunListenerorg.junit.internal.TextListener"这张图所用到的代码

我用 PlantUML 画了那张图,所用到的代码如下 ⬇️

puml 复制代码
@startuml

title <i>org.junit.runner.notification.RunListener</i> 和 <i>org.junit.internal.TextListener</i>

class org.junit.runner.notification.RunListener {
    + void testRunStarted(Description description) throws Exception
    + void testRunFinished(Result result) throws Exception
    + void testSuiteStarted(Description description) throws Exception
    + void testSuiteFinished(Description description) throws Exception
    + void testStarted(Description description) throws Exception
    + void testFinished(Description description) throws Exception
    + void testFailure(Failure failure) throws Exception
    + void testAssumptionFailure(Failure failure)
    + void testIgnored(Description description) throws Exception
}

org.junit.runner.notification.RunListener <|-- org.junit.internal.TextListener

class org.junit.internal.TextListener {
    -final PrintStream writer
    +TextListener(JUnitSystem system)
    +TextListener(PrintStream writer)
    +void testRunFinished(Result result)
    +void testStarted(Description description)
    +void testFailure(Failure failure)
    +void testIgnored(Description description)
    -PrintStream getWriter()
    #void printHeader(long runTime)
    #void printFailures(Result result)
    #void printFailure(Failure each, String prefix)
    #void printFooter(Result result)
    #String elapsedTimeAsString(long runTime)
}
@enduml
相关推荐
派大星-?7 小时前
自动化测试五模块一框架(上)
开发语言·python·测试工具·单元测试·可用性测试
少云清1 天前
【UI自动化测试】2_PO模式 _单元测试框架(重点)
ui·单元测试·po模式
清水白石0081 天前
测试金字塔实战:单元测试、集成测试与E2E测试的边界与平衡
python·单元测试·log4j·集成测试
百锦再2 天前
Java中的日期时间API详解:从Date、Calendar到现代时间体系
java·开发语言·spring boot·struts·spring cloud·junit·kafka
天真小巫2 天前
2026.2.20总结(认识自我)
单元测试·压力测试
阿林来了3 天前
Flutter三方库适配OpenHarmony【flutter_speech】— 单元测试与集成测试
flutter·单元测试·集成测试·harmonyos
米羊1213 天前
Log4j
单元测试
七夜zippoe3 天前
单元测试进阶:pytest高级特性与实战秘籍
单元测试·pytest·持续集成·猴子补丁·fixtrue
金銀銅鐵3 天前
浅解 Junit 4 第七篇:AllDefaultPossibilitiesBuilder
java·junit·单元测试