使用 Java 測試小工具測試資料工廠從 JUnit 測試中的檔案載入測試數據
1.概述
在編寫 JUnit 測試時,我們可能需要將測試資料用作程式碼的輸入或預期輸出。我們可以透過在測試中或測試資料工廠類別中實例化 Java 物件來實現這一點,但在某些情況下,建立包含測試資料的檔案並在測試期間載入它們更容易。
在本教程中,我們將了解如何從檔案系統載入測試數據,並了解Java Test Gadgets如何使用其適用於 JUnit 4 和 JUnit 5 的Test Data Factory插件來解決此問題。
2.範例
讓我們來看一個文件中的測試資料可能有用的例子。
2.1.文字轉換器
假設我們正在創建一個模組來載入文字進行處理。它有一個模型,將Paragraphs
儲存在Document
中,將Sentence
儲存在Paragraph
中,將Token
儲存在Sentence
中:
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.
我們可能希望將其解析為可以儲存在.json
文件中的Document
:
{
"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"));
}
}
}
我們應該注意,這個函數會拋出一個已檢查的IOException
,除非我們明確地加入一個catch
區塊來使用RuntimeException
重新拋出。
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. 測試資料工廠 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. 測試資料工廠 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.延遲加載
如果我們有很多文件,那麼在每次測試之前創建數十個欄位並加載所有欄位可能會很耗時。因此,我們可以使用它來注入Supplier
,而不是使用@TestData
來注入檔案的值:
@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.支援文件格式
預設情況下,測試資料工廠僅支援.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 類別規則
要使用 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. 結論
在本文中,我們研究了使用資料檔案儲存測試資料而不是以程式設計方式建立測試資料的好處。
我們了解如何在沒有任何框架幫助的情況下載入測試資料。然後我們研究了測試資料工廠 JUnit 4 插件和 JUnit 4 擴展,它允許我們以聲明方式載入測試資料。
我們了解如何使用測試資料集合來模組化類似的測試資料集,以及如何在測試之間提供共用載入器以便可以快取資料。