背景
按照我个人的理解,JUnit 4 最核心的类是以下四个
我们在前面的一些文章里已经专门探讨过 TestClass/Runner/RunnerBuilder ⬇️
TestClassRunnerRunnerBuilder
本文会初步探讨 Statement 这个类背后的思想。
正文
生活中的例子:播放电视剧
我们从生活中的例子入手。现在需要写一些类,用于模拟播放一部电视剧的过程。在写代码之前,我们可以先思考一下 ⬇️
- 无论是播放 一整部剧 还是只播放其中的 一集,都有“播放”这个行为。所以我们可以给它们定义一个公共的接口
Player - 播放一整部剧时,需要把这部剧包含的每一集播放出来,所以这里可以借鉴 组合模式 或者 迭代器模式 的思想(代码不必太拘泥,有它们的思想即可)
- 每一集在播放时,可能又有各自的特殊逻辑,例如第一集需要播放广告,第二集没有广告之类,所以这里可以借鉴 装饰器模式 的思想(代码不必太拘泥,有它的思想即可),播放每一集的行为还是用
Player这个接口来描述,但我们可以用不同的子类来封装这些特殊逻辑
基于这些思考,我写出了如下的代码(为了行文方便,我把所有类都放在一个 java 文件里了,但是日常开发时,一般不会这样做)
import java.util.List;
/**
* 这个接口用于表示播放电视剧(可以表示播放一集, 也可以表示播放完整的一部剧)
*/
interface Player {
/**
* 播放(可以播放一集, 也可以播放完整的一部剧)
*/
void play();
}
/**
* 这个类负责播放完整的一部剧
*/
class TVSeriesPlayer implements Player {
/**
* 每个 child 负责播放剧中的一集
*/
private final List<Player> children;
public TVSeriesPlayer(List<Player> episodePlayerList) {
this.children = episodePlayerList;
}
@Override
public void play() {
boolean isFirstPlayer = true;
for (Player childPlayer : children) {
if (!isFirstPlayer) {
System.out.println();
}
isFirstPlayer = false;
childPlayer.play();
}
}
}
/**
* 这个类负责播放一集电视剧
*/
class EpisodePlayer implements Player {
private final String description;
public EpisodePlayer(String description) {
this.description = description;
}
@Override
public void play() {
doPlayOneEpisode();
}
private void doPlayOneEpisode() {
System.out.printf(String.format("播放电视剧: [%s]%n", description));
System.out.printf(String.format("[%s] 播放完了 %n", description));
}
}
/**
* 这个类负责播放一集电视剧, 且在播放它之前先播放广告
*/
class WithPreAdPlayer implements Player {
private final Player nextPlayer;
public WithPreAdPlayer(Player nextPlayer) {
this.nextPlayer = nextPlayer;
}
private void playAdvertisementBeforeEpisode() {
System.out.println("先播放了广告 x");
}
@Override
public void play() {
// 在播放电视剧之前, 先播放广告
playAdvertisementBeforeEpisode();
nextPlayer.play();
}
}
/**
* 这个类负责播放一集电视剧, 且在播放它之后播放广告
*/
class WithPostAdPlayer implements Player {
private final Player nextPlayer;
public WithPostAdPlayer(Player nextPlayer) {
this.nextPlayer = nextPlayer;
}
private void playAdvertisementBeforeEpisode() {
System.out.println("之后播放了广告 y");
}
@Override
public void play() {
nextPlayer.play();
// 在播放电视剧之后, 播放广告
playAdvertisementBeforeEpisode();
}
}
class DailyLifeExample {
public static void main(String[] args) {
Player playerForEpisode1 = new WithPostAdPlayer(
new WithPreAdPlayer(new EpisodePlayer("第一集"))
);
Player playerForEpisode2 = new EpisodePlayer("第二集"); // 假设第二集不播放广告
Player playerForEpisode3 = new WithPreAdPlayer(new EpisodePlayer("第三集"));
List<Player> childPlayers = // 假设第三集很精彩, 要播放两次
List.of(playerForEpisode1, playerForEpisode2, playerForEpisode3, playerForEpisode3);
Player player = new TVSeriesPlayer(childPlayers);
player.play();
}
}
请将以上代码保存为 DailyLifeExample.java。
编译和运行
执行下方的命令就可以编译 DailyLifeExample.java 以及运行其中的 main 方法
javac DailyLifeExample.java
java DailyLifeExample
运行结果如下 ⬇️
先播放了广告 x
播放电视剧: [第一集]
[第一集] 播放完了
之后播放了广告 y
播放电视剧: [第二集]
[第二集] 播放完了
先播放了广告 x
播放电视剧: [第三集]
[第三集] 播放完了
先播放了广告 x
播放电视剧: [第三集]
[第三集] 播放完了
从输出结果来看,程序的运行符合预期
- 在播放第一集前后都要播放广告
- 在播放第二集前后都不要播放广告
- 在播放第三集之前要播放广告(之后不播放广告)
- 第三集要播放两次
分析
我用 Work Breakdown Structure 展示了调用 player.play() 时发生了什么 ⬇️
只要把这个例子看明白,其实也就明白了 的核心思想。 的内容如下 ⬇️
其实它和我们定义的 Player 接口很相似,区别在于
Player接口是对播放行为的抽象Statement抽象类是对测试时发生的action的抽象
在此基础上再去看 @Before/@After/@Before/@AfterClass 是如何生效的,应该就不难了。我在以下文章中,探讨了 JUnit 4 处理这四个注解的方式,有兴趣的读者可以参考。
其他
“背景”里的类图是如何画出来的?
我是用 PlantUML 的插件画的,代码如下
@startuml
title <i>JUnit 4</i> 中 <i>4</i> 个核心类的简要类图
interface org.junit.runners.model.Annotatable
class org.junit.runners.model.TestClass
interface org.junit.runner.Describable
abstract org.junit.runner.Runner
abstract org.junit.runners.model.RunnerBuilder
abstract org.junit.runners.model.Statement
org.junit.runners.model.Annotatable <|.. org.junit.runners.model.TestClass
org.junit.runner.Describable <|.. org.junit.runner.Runner
note "我觉得这四个类是核心的类" as N #orange
N -- org.junit.runners.model.TestClass #orange
N -- org.junit.runner.Runner #orange
N -- org.junit.runners.model.RunnerBuilder #orange
N -- org.junit.runners.model.Statement #orange
header 我认为 <i>JUnit 4</i> 中的核心类有以下四个\n<&star><i>TestClass</i>\n<&star><i>Runner</i>\n<&star><i>RunnerBuilder</i>\n<&star><i>Statement</i>
@enduml
“模拟播放电视剧行为, 所用到的各个类的类图”是如何画出来的?
我是用 PlantUML 的插件画的,代码如下
@startuml
title 模拟播放电视剧行为, 所用到的各个类的类图
caption \n\n
' caption 里的内容只是为了防止掘金平台自动生成的水印遮盖图中的内容
interface Player
class TVSeriesPlayer
class EpisodePlayer
class WithPreAdPlayer
class WithPostAdPlayer
interface Player {
void play()
}
Player <|.. TVSeriesPlayer
Player <|.. EpisodePlayer
Player <|.. WithPreAdPlayer
Player <|.. WithPostAdPlayer
class TVSeriesPlayer {
- final List<Player> children
+ TVSeriesPlayer(List<Player> episodePlayerList)
+ void play()
}
class EpisodePlayer {
- final String description
+ EpisodePlayer(String description)
+ void play()
- void doPlayOneEpisode()
}
class WithPostAdPlayer {
- final Player nextPlayer
+ WithPostAdPlayer(Player nextPlayer)
- void playAdvertisementBeforeEpisode()
+ void play()
}
class WithPreAdPlayer {
- final Player nextPlayer
+ WithPreAdPlayer(Player nextPlayer)
- void playAdvertisementBeforeEpisode()
+ void play()
}
note right of Player
这个接口用于表示播放电视剧
(可以表示播放一集, 也可以表示播放完整的一部剧)
end note
note bottom of TVSeriesPlayer
这个类负责播放完整的 <b>一部剧</b>
end note
note bottom of EpisodePlayer
这个类负责播放 <b>一集</b> 电视剧
end note
note bottom of WithPreAdPlayer
这个类负责播放一集电视剧,
且在播放它 <b>之前</b> 先播放广告
end note
note bottom of WithPostAdPlayer
这个类负责播放一集电视剧,
且在播放它 <b>之后</b> 播放广告
end note
@enduml
“调用 player.play() 时发生了什么”一图是如何画出来的?
我是用 PlantUML 的插件画的,代码如下
@startwbs
'https://plantuml.com/wbs-diagram
title 调用 <i>player.play()</i> 时发生了什么
* 调用 <i>player.play()</i>
**:第一个 <i>child</i> 执行 <i>play()</i> 方法
(我们把第一个 <i>child</i> 简称为 <i>c<sub>1</sub></i>,
它的精确类型是 <i>WithPostAdPlayer</i>)
<i>c<sub>1</sub></i> 的 <i>play()</i> 方法所做的事情 <:point_down:>;
***:调用 <i>nextPlayer.play()</i>
(<i>c<sub>1</sub></i> 中有 <i>nextPlayer</i> 字段,
后者的精确类型是 <i>WithPreAdPlayer</i>,
我们将后者简称为 <b><i>Player<sub>甲</sub></i></b>)
<i>Player<sub>甲</sub>.play()</i> 所做的事情 <:point_down:>;
****[#lightgreen] 播放前置的广告
****:调用 <i>nextPlayer.play()</i>
(<i>Player<sub>甲</sub></i> 里也有一个 <i>nextPlayer</i> 字段,
后者的精确类型是 <i>EpisodePlayer</i>,
我们将后者简称为 <b><i>Player<sub>乙</sub></i></b>)
<i>Player<sub>乙</sub>.play()</i> 所做的事情 <:point_down:>;
*****[#orange] 播放第一集
***[#lightgreen] 播放后置的广告
**:第二个 <i>child</i> 执行 <i>play()</i> 方法
(我们把第二个 <i>child</i> 简称为 <i>c<sub>2</sub></i>,
它的精确类型是 <i>EpisodePlayer</i>)
<i>c<sub>2</sub></i> 的 <i>play()</i> 方法所做的事情 <:point_down:>;
***[#orange] 播放第二集
**:第三个 <i>child</i> 执行 <i>play()</i> 方法
(我们把第三个 <i>child</i> 简称为 <i>c<sub>3</sub></i>,
它的精确类型是 <i>WithPreAdPlayer</i>)
<i>c<sub>3</sub></i> 的 <i>play()</i> 方法所做的事情 <:point_down:>;
***[#lightgreen] 播放前置的广告
***:调用 <i>nextPlayer.play()</i>
(<i>c<sub>3</sub></i> 中有 <i>nextPlayer</i> 字段,
后者的精确类型是 <i>EpisodePlayer</i>
我们将后者简称为 <b><i>Player<sub>丙</sub></i></b>)
<i>Player<sub>丙</sub>.play()</i> 方法所做的事情 <:point_down:>;
****[#orange] 播放第三集
**:第四个 <i>child</i> 执行 <i>play()</i> 方法
(第四个 <i>child</i> 和第三个 <i>child</i> 是同一个对象,
所以就不必再分析一遍了);
***_ ...
header 播放某一集的行为用 <b>橙色</b> 的节点来表示\n播放广告的行为用 <b>浅绿色</b> 的节点来表示
@endwbs