从 JUnit 测试中的文件加载测试数据,使用 Java Test Gadgets 测试数据工厂
最后更新:2025年5月23日
1. 概述
在编写 JUnit 测试时,我们可能需要创建测试数据作为我们代码的输入或期望输出。我们可以通过在测试中实例化 Java 对象,或在测试数据工厂类中来完成此操作,但在某些情况下,创建包含我们测试数据的文件并在测试期间加载它们会更容易。
在本教程中,我们将了解如何从文件系统加载测试数据,并学习 Java Test Gadgets 如何使用其 测试数据工厂 插件为 JUnit 4 和 JUnit 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,我们可以使用 Jackson 的 ObjectMapper 加载 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. 定义集合的目录
我们可能希望在基于场景的目录中拥有具有相同名称的多个文件集
我们可以定义一个测试数据集合接口来表示这些接口
@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 会员 身份登录,就开始学习并在项目上进行编码。















