背景
按照我个人的理解,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> children) {
this.children = children;
}
@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 {
/**
* 更内层的 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 {
/**
* 更内层的 player
*/
private final Player nextPlayer;
public WithPostAdPlayer(Player nextPlayer) {
this.nextPlayer = nextPlayer;
}
private void playAdvertisementAfterEpisode() {
System.out.println("之后播放了广告 y");
}
@Override
public void play() {
nextPlayer.play();
// 在播放电视剧之后, 播放广告
playAdvertisementAfterEpisode();
}
}
class DailyLifeExample {
public static void main(String[] args) {
// 在播放第一集前后都播放广告
Player episode1Player = new WithPostAdPlayer(
new WithPreAdPlayer(new EpisodePlayer("第一集"))
);
// 假设第二集不播放广告
Player episode2Player = new EpisodePlayer("第二集");
// 播放第三集之前播放广告
Player episode3Player = new WithPreAdPlayer(new EpisodePlayer("第三集"));
// 假设第三集很精彩, 要播放两次
List<Player> childPlayers =
List.of(episode1Player, episode2Player, episode3Player, episode3Player);
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() 时发生了什么 ⬇️
和 JUnit 4 框架中相关类的对比
只要把上面的例子看明白,其实也就明白了 的核心思想。 的内容如下 ⬇️
其实它和我们定义的 Player 接口很相似,区别在于
Player接口是对播放行为的抽象Statement抽象类是对测试时发生的action的抽象
JUnit 4 框架中提供了 的一些子类,用于处理前置/后置(类似于本例中的播放广告)的逻辑。
我把本例中的一些类和 JUnit 4 框架中的一些类进行了类比 ⬇️
| 本例中的类 | JUnit 4 框架中与之类似的类 |
|---|---|
| Player | |
| EpisodePlayer | |
| WithPreAdPlayer | |
| WithPostAdPlayer |
对应的类图如下 ⬇️
在此基础上再去看 @Before/@After/@BeforeClass/@AfterClass 是如何生效的,应该就不难了。我在以下文章中,探讨了 JUnit 4 处理 @Before/@After/@BeforeClass/@AfterClass 注解的方式,有兴趣的读者可以参考。
其他
文中用到的很多图是用 PlantUML 的插件绘制的。这篇笔记 汇总了画这些图所用到的原始代码。
相关文章
TestClassRunnerRunnerBuilderStatement的应用