电子书 – Spring Cloud 指南 – NPI EA (分类=Spring Cloud)
announcement - icon

让我们开始使用 Spring Cloud 的微服务架构

>> 加入 Pro 并下载电子书

电子书 – Mockito – NPI EA (标签 = Mockito)
announcement - icon

模拟是单元测试的重要组成部分,Mockito 库使编写 清晰直观的单元测试 变得容易,用于您的 Java 代码。

通过我们的 Mockito 指南 开始模拟,并改进您的应用程序测试

下载电子书

电子书 – Java 并发 – NPI EA (分类=Java 并发)
announcement - icon

在应用程序中处理并发可能是一个棘手的过程,其中包含许多 潜在的陷阱。 扎实的掌握基本知识将有助于最大程度地减少这些问题。

通过我们的 Java 并发 指南开始了解多线程应用程序

>> 下载电子书

电子书 – 响应式 – NPI EA (分类=响应式)
announcement - icon

Spring 5 增加了对使用 Spring WebFlux 模块进行响应式编程的支持,此支持自那时起不断改进。 开始使用 Reactor 项目基础知识和 Spring Boot 中的响应式编程

>> 加入 Pro 并下载电子书

电子书 – Java Streams – NPI EA (分类=Java Streams)
announcement - icon

自从 Java 8 引入以来,Stream API 已成为 Java 开发的基础。 基本操作,例如迭代、过滤、映射元素序列,使用起来看似很简单。

但这些也可能被过度使用并陷入一些常见陷阱。

更好地了解 Stream 的工作方式 以及如何将其与其他语言功能结合使用,请查看我们关于 Java Streams 的指南

>> 加入 Pro 并下载电子书

电子书 – Jackson – NPI EA (分类=Jackson)
announcement - icon

用 Jackson 正确处理 JSON

下载电子书

电子书 – HTTP 客户端 – NPI EA (分类=Http 客户端)
announcement - icon

充分利用 Apache HTTP 客户端

下载电子书

电子书 – Maven – NPI EA (分类 = Maven)
announcement - icon

开始使用 Apache Maven

下载电子书

电子书 – 持久化 – NPI EA (分类=持久化)
announcement - icon

您在努力实现正确的持久化层 Spring 吗?

探索电子书

电子书 – RwS – NPI EA (分类=Spring MVC)
announcement - icon

使用 Spring 构建 REST API 吗?

下载电子书

课程 – LS – NPI EA (分类=Jackson)
announcement - icon

通过 Learn Spring 课程开始学习 Spring 和 Spring Boot

>> 学习 SPRING
课程 – RWSB – NPI EA (分类=REST)
announcement - icon

通过构建一个完整的 REST API,深入了解 Spring Boot 3 和 Spring 6,使用该框架

>> 全新的“REST With Spring Boot”

课程 – LSS – NPI EA (分类=Spring Security)
announcement - icon

是的,Spring Security 可能很复杂,从核心内的更高级功能到框架中深入的 OAuth 支持。

我将安全材料构建为 两个完整的课程 - 核心和 OAuth,以针对这些更复杂的场景进行实践。 我们探索何时以及如何使用每个功能,并 在后台项目中对其进行编码

您可以在这里探索该课程

>> 学习 Spring Security

课程 – LSD – NPI EA (标签=Spring Data JPA)
announcement - icon

Spring Data JPA 是处理 JPA 复杂性的绝佳方式,它具有 Spring Boot 的强大简洁性

通过引导式参考课程开始使用 Spring Data JPA

>> 查看课程

合作伙伴 – Moderne – NPI EA (类别=Spring Boot)
announcement - icon

使用 OpenRewrite 安全且自动地重构 Java 代码。

手动重构大型代码库既缓慢、有风险,又容易拖延。OpenRewrite 应运而生。这个用于大规模、自动化代码转换的开源框架可以帮助团队安全、一致地进行现代化改造。

每个月,OpenRewrite 的创建者和维护者 Moderne 都会举办现场、实践培训课程——一个面向初学者,一个面向经验丰富的用户。您将了解配方的运作方式、如何将其应用于项目,以及如何自信地进行代码现代化改造。

参加下一次课程,带来您的问题,并学习如何自动化通常会占用您 sprint 时间的工作。

合作伙伴 – LambdaTest – NPI EA (类别=测试)
announcement - icon

回归测试是发布流程中的重要步骤,以确保新代码不会破坏现有功能。随着代码库的不断发展,我们希望频繁运行这些测试,以便尽早发现任何问题。

确保这些测试以自动化的方式频繁运行的最佳方法当然是将其包含在 CI/CD 管道中。 这样,每次向仓库提交代码时,回归测试将自动执行。

在本教程中,我们将学习如何使用 Selenium 创建回归测试,然后使用 GitHub Actions 将它们包含在我们的管道中,在 LambdaTest 云网格上运行

>> 如何使用 GitHub Actions 运行 Selenium 回归测试

课程 – LJB – NPI EA (类别 = Core Java)
announcement - icon

通过编码方式构建 Java 的坚实、实用的基础

>> 学习 Java 基础

合作伙伴 – LambdaTest – NPI (类别 = 测试)
announcement - icon

回归测试是发布流程中的重要步骤,以确保新代码不会破坏现有功能。随着代码库的不断发展,我们希望频繁运行这些测试,以便尽早发现任何问题。

确保这些测试以自动化的方式频繁运行的最佳方法当然是将其包含在 CI/CD 管道中。 这样,每次向仓库提交代码时,回归测试将自动执行。

在本教程中,我们将学习如何使用 Selenium 创建回归测试,然后使用 GitHub Actions 将它们包含在我们的管道中,在 LambdaTest 云网格上运行

>> 如何使用 GitHub Actions 运行 Selenium 回归测试

课程 – LJU – NPI (标签 = JUnit)
announcement - icon

通过Learn JUnit课程掌握最流行的 Java 测试框架

>> 学习 JUnit

1. 概述

在编写 JUnit 测试时,我们可能需要创建测试数据作为我们代码的输入或期望输出。我们可以通过在测试中实例化 Java 对象,或在测试数据工厂类中来完成此操作,但在某些情况下,创建包含我们测试数据的文件并在测试期间加载它们会更容易。

在本教程中,我们将了解如何从文件系统加载测试数据,并学习 Java Test Gadgets 如何使用其 测试数据工厂 插件为 JUnit 4JUnit 5 解决这个问题。

2. 示例

让我们看一个可能需要将测试数据保存在文件中的示例。

2.1. 文本转换器

假设我们正在创建一个模块来加载文本进行处理。它有一个模型,用于在 Document 中存储 Paragraphs,在 Paragraph 中存储 Sentence,以及在 Sentence 中存储 Token

public class Document {
    private List<Paragraph> paragraphs;
}

public class Paragraph {
    public enum Style { NORMAL, HEADING };

    private List<Sentence> sentences;
    private Style style = Style.NORMAL;
}

public class Sentence {
    private List<String> tokens;
}

我们希望编写一个用于在 .txt.md 格式的文件之间进行转换的工具,并希望使用测试驱动开发来完成我们的桩实现

public class Converter {
    public static Document fromText(String text) {
        // TO DO
    }

    public static Document fromMarkdown(String markdown) {
        // TO DO
    }

    public static String fromDocument(Document doc) {
        // TO DO
    }

    public static String toMarkdown(Document doc) {
        // TO DO
    }
}

我们可以在 src/test/resources/testdata 目录中存储一个文本文件 plain.txt 以供此测试使用

Paragraph one starts here.
Then paragraph two follows. It has two sentences.

我们可能期望将其解析为 Document,该 Document 可以存储在 .json 文件中

{
  "paragraphs": [
    {
      "style": "NORMAL",
      "sentences": [
        {
          "tokens": ["Paragraph", "one", "starts", "here."]
        }
      ]
    },
    {
      "style": "NORMAL",
      "sentences": [
        {
          "tokens": ["Then", "paragraph", "two", "follows."]
        },
        {
          "tokens": ["It", "has", "two", "sentences."]
        }
      ]
    }
  ]
}

2.2. 与本地对象比较

我们可以使用 TestDataFactory 类中的纯 Java 来构建测试数据,而不是数据文件

public class TestDataFactory {
    public static String twoParagraphs() {
        return "Paragraph one starts here.\n" +
          "Then paragraph two follows. It has two sentences.";
    }
}

对于字符串来说,它很轻量级,但较长的文档可能会导致很大的 .java 文件。

但是,构建我们的 Document 对象需要更多的代码

public static Document twoParagraphsAsDocument() {
    Paragraph paragraph1 = new Paragraph();
    paragraph1.setStyle(Paragraph.Style.NORMAL);

    Sentence sentence1 = new Sentence();
    sentence1.setTokens(asList("Paragraph", "one", "starts", "here."));
    paragraph1.setSentences(asList(sentence1));

    Paragraph paragraph2 = new Paragraph();
    paragraph2.setStyle(Paragraph.Style.NORMAL);
    Sentence sentence2 = new Sentence();
    sentence2.setTokens(asList("Then", "paragraph", "two", "follows."));
    Sentence sentence3 = new Sentence();
    sentence3.setTokens(asList("It", "has", "two", "sentences."));
    paragraph2.setSentences(asList(sentence2, sentence3));

    Document document = new Document();
    document.setParagraphs(asList(paragraph1, paragraph2));
    return document;
}

我们可以通过添加构建器或特殊的构造函数来简化此代码,但数据文件会更容易。

2.3. 我们在测试中使用测试数据文件时需要的功能

在使用来自文件的测试数据时,我们需要

  • 从文件到正确类型的反序列化
  • 检查异常处理——尤其是 IOException ——而不会使我们的测试代码变得混乱
  • 重新加载,以便我们在测试期间更改了数据
  • 避免不必要的重新加载文件带来的性能成本
  • 处理跨多个操作系统的文件路径

3. 纯 Java 中的测试数据文件

我们可以创建一个测试数据工厂,从文件系统加载文件。

3.1. 计算路径

我们需要能够在 src/test/resources 中表达文件的路径,而无需使用特定于系统的 文件分隔符

Path path = Paths.get("src", "test", "resources",
  "testdata", "twoParagraphs.txt");

3.2. 加载纯文本

然后我们可以使用 Files.lines() 从该路径加载纯文本文件

public class TestDataFilesFactory {
    public static String twoParagraphs() throws IOException {
        Path path = Paths.get("src", "test", "resources",
          "testdata", "twoParagraphs.txt");
        try (Stream<String> file = Files.lines(path)) {
            return file.collect(Collectors.joining("\n"));
        }
    }
}

我们应该注意到,除非我们显式添加一个 catch 块以使用 RuntimeException 重新抛出,否则此函数会抛出一个检查型 IOException

3.3. 加载 JSON

对于 Document,我们可以使用 JacksonObjectMapper 加载 JSON

public static Document twoParagraphsAsDocument() throws IOException {
    ObjectMapper objectMapper = new ObjectMapper();
    return objectMapper.readValue(
      Paths.get("src", "test", "resources",
        "testdata", "twoParagraphs.json").toFile(), Document.class);
}

3.4. 在测试中使用加载的文件

我们可以在单元测试中使用这些加载的值

@Test
void givenDocumentAndPlaintextInFiles_whenConvertToText_thenMatches() throws IOException {
    Document source = TestDataFilesFactory.twoParagraphsAsDocument();

    String asPlaintext = TestDataFilesFactory.twoParagraphs();

    assertThat(Converter.fromDocument(source)).isEqualTo(asPlaintext);
}

3.5. 这种方法的局限性

这段代码并不特别复杂,但是有很多样板代码。纯文本文件的不可变 String 需要为每个测试加载。虽然我们可以使用静态字段,但我们必须处理 IOException 来初始化它。

导航路径结构的样板代码也需要重复或小心编码。

如果我们可以直接声明我们想要的数据,并将其注入到我们的测试中,那就容易多了。

4. Test Data Factory JUnit 4

4.1. 依赖项

要使用它,我们需要 test-gadgets 依赖

<dependency>
    <groupId>uk.org.webcompere</groupId>
    <artifactId>test-gadgets-junit4</artifactId>
    <version>1.0.2</version>
    <scope>test</scope>
</dependency>

4.2. 添加到 JUnit 4 测试

TestDataFieldsRule 允许将测试中的字段从文件中注入

@Rule
public TestDataFieldsRule rule = new TestDataFieldsRule(new TestDataLoader().addPath("testdata"));

如果未提供TestDataLoader,该规则将创建它自己的TestDataLoader,但这里我们添加了一个加载器对象,它期望我们的文件存储在testdata 子目录中。

然后,要将.json 文件注入到 POJO 中,我们可以声明一个带有@TestData 注释的字段

@TestData
private Document twoParagraphs;

它使用默认文件扩展名(.json),并假设文件名和字段名称匹配。因此,它将twoParagraphs.json 加载到Document 中。如果文件具有不同的扩展名,我们可以在 @TestData 注释中提供文件名

@TestData("twoParagraphs.txt")
private String twoParagraphsText;

如果有子目录,我们可以将它们表示为注释中的字符串数组。

这意味着我们的单元测试现在可以使用这些字段进行断言

assertThat(Converter.fromDocument(twoParagraphs)).isEqualTo(twoParagraphsText);

这种方法需要最少的样板代码。

5. Test Data Factory JUnit 5

5.1. 依赖项

我们首先将 依赖 添加到我们的pom.xml

<dependency>
    <groupId>uk.org.webcompere</groupId>
    <artifactId>test-gadgets-jupiter</artifactId>
    <version>1.0.2</version>
    <scope>test</scope>
</dependency>

5.2. 添加到 JUnit 5 测试

首先,我们使用 @TestDataFactory 注解我们的测试,并提供测试文件的子目录

@TestDataFactory(path = "testdata")
class ConverterTestFactoryFieldsJUnit5UnitTest {}

然后我们可以添加字段,如前所述使用@TestData 注解,并使用相同的单元测试来使用它们

@TestData
private Document twoParagraphs;

@TestData("twoParagraphs.txt")
private String twoParagraphsText;

@Test
void givenDocumentAndPlaintextInFiles_whenConvertToText_thenMatches() {
    assertThat(Converter.fromDocument(twoParagraphs)).isEqualTo(twoParagraphsText);
}

5.3. 参数注入

如果有很多测试使用不同的文件,我们可能更喜欢在逐个测试的基础上注入特定的数据

@Test
void givenInjectedFiles_whenConvertToText_thenMatches(
  @TestData("twoParagraphs.json") Document twoParagraphs,
  @TestData("twoParagraphs.txt") String twoParagraphsText) {
    // assertion
}

此测试的输入参数从注释描述的文件内容中分配。

6. 延迟加载

如果有很多文件,那么在每次测试之前创建几十个字段并加载所有这些字段可能很耗时。因此,与其使用@TestData 注入文件值,我们可以使用它来注入一个 Supplier

@TestData("twoParagraphs.txt")
private Supplier<String> twoParagraphsText;

然后在测试中使用Supplier 对象和get()

assertThat(Converter.fromDocument(twoParagraphs.get()))
  .isEqualTo(twoParagraphsText.get());

我们可以将其用于将所有可能的测试文件放入公共测试基类中的 Supplier 字段中,并在每个测试中使用所需的字段。但是,对于这种情况,有一个更好的解决方案。

7. 测试数据集合

7.1. 使用集合

如果我们要在多个地方使用相同的一组测试数据,或者在不同的目的的多个目录中拥有具有相同名称的文件组,那么我们可以声明一个测试数据集合来表示它们并注入它。我们首先定义一个带有@TestDataCollection 注解的接口,该接口具有每个文件的 getter 方法

@TestDataCollection
public interface TwoParagraphsCollection {
    @TestData("twoParagraphs.json")
    Document twoParagraphs();

    @TestData("twoParagraphs.txt")
    String twoParagraphsText();
}

然后我们将此接口注入到带有 @TestData 注解的测试对象中

@TestData
private TwoParagraphsCollection collection;

然后可以在测试用例中使用它

assertThat(Converter.fromDocument(collection.twoParagraphs()))
  .isEqualTo(collection.twoParagraphsText());

在 JUnit 5 中,这也作为注入的参数起作用

@Test
void givenInjectedCollection_whenConvertToText_thenMatches(
  @TestData TwoParagraphsCollection collection) {
    assertThat(Converter.fromDocument(collection.twoParagraphs()))
      .isEqualTo(collection.twoParagraphsText());
}

7.2. 定义集合的目录

我们可能希望在基于场景的目录中拥有具有相同名称的多个文件集

Screenshot 2025-05-14 at 08

我们可以定义一个测试数据集合接口来表示这些接口

@TestDataCollection
public interface AllVersions {
    @TestData("text.json")
    Document document();
    
    @TestData("text.md")
    String markdown();
    
    @TestData("text.txt")
    String text();
}

然后我们将正确的子目录放入 @TestData 注解中

@TestData("dickens")
private AllVersions dickens;

@TestData("shakespeare")
private AllVersions shakespeare;

8. 支持的文件格式

默认情况下,Test Data Factory 仅支持.txt 和 .json 文件。但是我们可以扩展它。

8.1. 使用现有加载器自定义 – JUnit 4

对于我们的 markdown 示例,我们希望支持加载 .md 文件为文本。在构造我们的TestDataLoader 时,我们可以为.md 添加一个映射。

@Rule
public TestDataFieldsRule rule = new TestDataFieldsRule(
  new TestDataLoader()
    .addLoader(".md", new TextLoader())
    .addPath("testdata"));

8.2. 使用现有加载器自定义 – JUnit 5

我们可以通过@TestDataFactory 注解为 JUnit 5 提供自定义加载设置

@TestDataFactory(
    loaders = { @FileTypeLoader(extension = ".md", loadedBy = TextLoader.class) },
    path = "testdata")

在这里,loaders 属性让我们能够将文件扩展名与加载类映射起来。加载类必须具有默认构造函数并实现 ObjectLoader 接口。

或者,我们可以在测试类中的静态字段中自定义加载器。它使用 @Loader 注解,以便扩展程序可以使用它

@TestDataFactory
class StaticLoaderUnitTest {
    @Loader
    private static TestDataLoader customLoader = new TestDataLoader()
      .addLoader(".md", new TextLoader())
      .addPath("testdata");
}

我们还可以访问扩展程序为我们创建的加载器——也许是为了进行一些临时文件加载。如果提供未初始化的字段,扩展程序会将它注入到我们的测试对象中

@Loader
private TestDataLoader loader;

8.3. 自定义加载器

我们还可以通过实现 ObjectLoader 接口来创建全新的加载器。或者,我们可以通过使用不同的映射器构造它来修改 JsonLoader 使用的 ObjectMapper

TestDataLoader customLoader = new TestDataLoader()
   .addLoader(".json", new JsonLoader(myObjectMapper));

这里我们使用 addLoader() 来为现有文件扩展名提供替换加载器。

9. 重用加载的数据

如果我们的许多测试都在使用相同的数据,并且在测试过程中没有更改它,那么最好不要一直从磁盘重新加载这些数据。通过在测试之间共享加载器,我们可以实现这一点。同样,我们可以使用测试数据工厂为静态字段提供值。

9.1. 使用 JUnit 4 Class Rule

要使用 JUnit 插件填充静态字段,我们需要使用 TestDataClassRule

@ClassRule
public static TestDataClassRule classRule = new TestDataClassRule(
  new TestDataLoader()
    .addLoader(".md", new TextLoader())
    .addPath("testdata"));

根据 JUnit 4 的标准,它使用 @ClassRule 注解,并针对测试类的静态字段

@TestData("twoParagraphs.txt")
private static String twoParagraphsTextStatic;

9.2. 使用 JUnit 5

@TestDataFactory 在类级别定义 TestDataLoader,并从中填充任何静态和非静态字段。

9.3. 不可变数据

我们应该将类的静态字段视为在测试之间共享。我们应该只将它们用于我们不打算更改的数据。

然而,我们可能有一些值,我们知道我们不会更改它们,并且我们希望为字段、测试数据集合或我们在测试中使用的 Supplier 对象以相同的方式提供它们。

由于 String 是不可变的,TestDataLoader 将自动提供相同的值,无论它被注入多少次

@TestData("twoParagraphs.txt")
private static String twoParagraphsTextStatic;

@TestData("twoParagraphs.txt")
private String twoParagraphsTextField;

// ...
assertThat(twoParagraphsTextStatic).isSameAs(twoParagraphsTextField);

对于其他类型的数据,我们需要显式地将其标记为防止更改。

9.4. 测试数据可能会更改

从文件读取以构建测试对象的一个优点是,我们可以从模板对其进行自定义。例如,在我们的 AllVersions 测试数据中,我们有一个 .md、一个 .txt 和一个 .json 具有相同的文本,我们可以使用它们来测试它们之间的转换。但是,虽然 .json.md 版本中的标题格式匹配,但 .txt 版本没有格式。

因此,我们可能会修改测试中的 Document 的临时副本以使它们匹配

Document document = shakespeare.document();
document.getParagraphs().get(0).setStyle(Paragraph.Style.NORMAL);
document.getParagraphs().get(1).setStyle(Paragraph.Style.NORMAL);
assertThat(Converter.fromText(shakespeare.text())).isEqualTo(document);

在这种情况下,我们受益于每个 Document 都是唯一的实例。

9.5. 请求缓存数据

但是,当知道测试数据不会更改时,我们可以将 immutability 模式添加到加载器或我们正在注入的项中

@TestData(value = "twoParagraphs.json", immutable = Immutable.IMMUTABLE)
private static Document twoParagraphsStaticImmutable;

@TestData(value = "twoParagraphs.json", immutable = Immutable.IMMUTABLE)
private Document twoParagraphsImmutable;

// ...
assertThat(twoParagraphsStaticImmutable).isSameAs(twoParagraphsImmutable);

在这里,我们证明了twoParagraphs.json 在提供给所有声明了@TestData 描述其为不可变性的字段之前,只会加载一次。

10. 结论

在本文中,我们探讨了使用数据文件来存储测试数据的好处,而不是以编程方式构建它。

我们看到如何在没有任何框架帮助的情况下加载测试数据。然后,我们研究了 Test Data Factory JUnit 4 插件和 JUnit 4 扩展,它们允许我们声明式地加载测试数据。

我们看到如何使用测试数据集合来模块化相似的测试数据集,以及如何提供一个共享加载器跨测试,以便可以缓存数据。

支持本文的代码可在 GitHub 上获取。 一旦你Baeldung Pro 会员 身份登录,就开始学习并在项目上进行编码。
Baeldung Pro – NPI EA (类别 = Baeldung)
announcement - icon

Baeldung Pro 具有完全无广告以及最终具有深色模式,提供干净的学习体验

>> 探索干净的 Baeldung

一旦早期采用者的席位全部用完,价格将上涨并保持在每年 33 美元。

电子书 – HTTP 客户端 – NPI EA (类别=HTTP 客户端)
announcement - icon

Apache HTTP Client 是一个非常强大的库,适用于简单和高级用例,在测试 HTTP 端点时尤其适用。 查看我们的指南,涵盖基本请求和响应处理,以及安全性、Cookie、超时等。

>> 下载电子书

电子书 – Java 并发 – NPI EA (分类=Java 并发)
announcement - icon

在应用程序中处理并发可能是一个棘手的过程,其中包含许多 潜在的陷阱。 扎实的掌握基本知识将有助于最大程度地减少这些问题。

通过我们的 Java 并发 指南开始了解多线程应用程序

>> 下载电子书

电子书 – Java Streams – NPI EA (分类=Java Streams)
announcement - icon

自从 Java 8 引入以来,Stream API 已成为 Java 开发的基础。 基本操作,例如迭代、过滤、映射元素序列,使用起来看似很简单。

但这些也可能被过度使用并陷入一些常见陷阱。

更好地了解 Stream 的工作方式 以及如何将其与其他语言功能结合使用,请查看我们关于 Java Streams 的指南

>> 加入 Pro 并下载电子书

电子书 – 持久化 – NPI EA (分类=持久化)
announcement - icon

您在努力实现正确的持久化层 Spring 吗?

探索电子书

课程 – LS – NPI EA (类别=REST)

announcement - icon

从 Spring Boot 开始,通过 Learn Spring 课程了解核心 Spring。

>> 查看课程

合作伙伴 – Moderne – NPI EA (标签=重构)
announcement - icon

现代 Java 团队行动迅速——但代码库并不总是跟上。 框架会发生变化,依赖关系会漂移,技术债务会累积,直到它开始拖慢交付速度。 OpenRewrite 就是为此而构建的:一个开源重构引擎,可在保持开发人员意图不变的同时自动化重复的代码更改。

由 Moderne 的 OpenRewrite 创建者和维护者领导的每月培训系列,将介绍实际的迁移和现代化模式。 无论您是重构配方的新手,还是准备编写自己的配方,您都将学习以安全且可扩展的方式进行重构的实用方法。

如果您曾经希望重构感觉像编写代码一样自然——并且一样快速——这是一个很好的起点

电子书 Jackson – NPI EA – 3 (类别 = Jackson)
© .