浅解 JUnit 4 第十九篇:用 Statement 来封装测试过程中的一个动作

65 阅读5分钟

背景

按照我个人的理解,JUnit 4 框架最核心的类是以下四个

  • org.junit.runners.model.TestClass\text{org.junit.runners.model.TestClass}
  • org.junit.runner.Runner\text{org.junit.runner.Runner}
  • org.junit.runners.model.RunnerBuilder\text{org.junit.runners.model.RunnerBuilder}
  • org.junit.runners.model.Statement\text{org.junit.runners.model.Statement}

image.png

我们在前面的一些文章里已经专门探讨过 TestClass/Runner/RunnerBuilder ⬇️

本文会初步探讨 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
播放电视剧: [第三集]
[第三集] 播放完了

从输出结果来看,程序的运行符合预期

  • 在播放第一集前后都要播放广告
  • 在播放第二集前后都不要播放广告
  • 在播放第三集之前要播放广告(之后不播放广告)
  • 第三集要播放两次

分析

image.png

我用 Work Breakdown Structure 展示了调用 player.play() 时发生了什么 ⬇️

image.png

JUnit 4 框架中相关类的对比

只要把上面的例子看明白,其实也就明白了 org.junit.runners.model.Statement\text{org.junit.runners.model.Statement} 的核心思想。org.junit.runners.model.Statement\text{org.junit.runners.model.Statement} 的内容如下 ⬇️

image.png

其实它和我们定义的 Player 接口很相似,区别在于

  • Player 接口是对播放行为的抽象
  • Statement 抽象类是对测试时发生的 action 的抽象

JUnit 4 框架中提供了 org.junit.runners.model.Statement\text{org.junit.runners.model.Statement} 的一些子类,用于处理前置/后置(类似于本例中的播放广告)的逻辑。

我把本例中的一些类和 JUnit 4 框架中的一些类进行了类比 ⬇️

本例中的类JUnit 4 框架中与之类似的类
Playerorg.junit.runners.model.Statement\text{org.junit.runners.model.Statement}
EpisodePlayerorg.junit.internal.runners.statements.InvokeMethod\text{org.junit.internal.runners.statements.InvokeMethod}
WithPreAdPlayerorg.junit.internal.runners.statements.RunBefores\text{org.junit.internal.runners.statements.RunBefores}
WithPostAdPlayerorg.junit.internal.runners.statements.RunAfters\text{org.junit.internal.runners.statements.RunAfters}

对应的类图如下 ⬇️ image.png

在此基础上再去看 @Before/@After/@BeforeClass/@AfterClass 是如何生效的,应该就不难了。我在以下文章中,探讨了 JUnit 4 处理 @Before/@After/@BeforeClass/@AfterClass 注解的方式,有兴趣的读者可以参考。

其他

文中用到的很多图是用 PlantUML 的插件绘制的。这篇笔记 汇总了画这些图所用到的原始代码。

相关文章