這篇紀錄使用 DBUnit 進行跟 Database 有關的 Unit Test 的方法,
在這裡我使用的是
JUnit 5 版本 + JDK 11。
DBUnit 是一個可以幫助我們對專案資料庫 (Database) 部份進行單元測試的 (Unit Test) 的工具,
在實務上我們通常會希望能以真實的資料庫連線來進行測試 DAO Class,不不是只是用 Mock 的方式來模擬資料庫的輸入輸出。
DBUnit 的中心思想就是在做測試之前把當下的 Database 資料輸出成 XML 或 XLS 檔備份起來 (可以用程式輸出、也可以自己手寫編輯製作),
並準備一份測試用的資料檔案 (跟備份的一樣,只是是另一個檔案,當然你要用一樣的也行),
然後在每個測試要執行之前,都先把測試資料寫入 Database 中 (預設通常是會先把 Database 相關的 Table 資料刪掉,可以設定其他行為) 再進行測試,
等最後全部的測試案例都測試完畢後,再把備份的資料寫回 Database 以復原資料。
雖然可以用備份資料復原 Database 的資料,但並不保證會跟之前完全一模一樣,例如可能地增值沒設對、外鍵設定跑掉、資料重寫後有問題跟之前不一樣、資料回復失敗丟失資料等,所以不建議在正式線上環境下使用,只在測試環境下用就好。
以下直接展示使用的測試程式範例:
Maven Dependency 設定範例:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>hugo</groupId> <artifactId>test</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <maven.compiler.target>11</maven.compiler.target> <maven.compiler.source>11</maven.compiler.source> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.junit</groupId> <artifactId>junit-bom</artifactId> <version>5.10.3</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <scope>test</scope> </dependency> <!-- https://mvnrepository.com/artifact/org.dbunit/dbunit --> <dependency> <groupId>org.dbunit</groupId> <artifactId>dbunit</artifactId> <version>2.8.0</version> <scope>test</scope> </dependency> <!-- DBUnit 輸出 XLS Dataset 需要 4.1.1 (4.1.2 好像也可以) 版本的 poi --> <!-- https://mvnrepository.com/artifact/org.apache.poi/poi --> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>4.1.1</version> </dependency> <!-- 此例以 PostgreSQL 為例,所以有使用 PostgreSQL Driver 的 Dependency --> <!-- https://mvnrepository.com/artifact/org.postgresql/postgresql --> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.7.1</version> </dependency> </dependencies> </project>
單元測試主程式 :
package test; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Paths; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import org.dbunit.DatabaseUnitException; import org.dbunit.database.AmbiguousTableNameException; import org.dbunit.database.DatabaseConfig; import org.dbunit.database.DatabaseConnection; import org.dbunit.database.IDatabaseConnection; import org.dbunit.database.QueryDataSet; import org.dbunit.database.search.TablesDependencyHelper; import org.dbunit.dataset.DataSetException; import org.dbunit.dataset.IDataSet; import org.dbunit.dataset.excel.XlsDataSet; import org.dbunit.dataset.xml.FlatXmlDataSet; import org.dbunit.dataset.xml.FlatXmlDataSetBuilder; import org.dbunit.operation.DatabaseOperation; import org.dbunit.util.search.SearchException; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class DBUnitTest { //此例以 PostgreSQL 為例 static String dbUrl = "jdbc:postgresql://localhost:5432/xxxDatabase"; static String dbUsername = "username"; static String dbPasswordString = "password"; static String jdbcDriverName = "org.postgresql.Driver"; static String testResourceFolderPath = ""; //此例以 XLS 做為 Dataset file 為例,因為 XML 有特殊字完 \v (\u00B), Surrogate Pair Emoji 等無法處理正確的問題 static String backupDatasetResourcePath = "/backupDataset.xls"; static String testDatasetResourcePath = "/testDataset.xls"; static String backupDatasetResourceFilePath; static String testDatasetResourceFilePath; static IDatabaseConnection getIDatabaseConnection() { IDatabaseConnection iDatabaseConnection = null; try { Class.forName(jdbcDriverName); Connection jdbcConnection = DriverManager.getConnection(dbUrl, dbUsername, dbPasswordString); iDatabaseConnection = new DatabaseConnection(jdbcConnection); //這邊我多設定了一條 PROPERTY_ESCAPE_PATTERN 的設定, //它可以讓 DBUnit 在 select database table column 的名字時,用你自訂的 pattern 來做 select, //例如我希望用雙引號包住 column name,例如 desc 是 sql 關鍵字,當成 column name 做 select 是要用雙引號包住, //例子: select "desc" from xxxTable, //這時就可以設定 PROPERTY_ESCAPE_PATTERN 值為 "\"?\"" DatabaseConfig databaseConfig = iDatabaseConnection.getConfig(); databaseConfig.setProperty(DatabaseConfig.PROPERTY_ESCAPE_PATTERN, "\"?\""); databaseConfig.setProperty(DatabaseConfig.FEATURE_ALLOW_EMPTY_FIELDS, "true"); } catch (ClassNotFoundException | SQLException | DatabaseUnitException e) { e.printStackTrace(); } return iDatabaseConnection; } @BeforeAll static void beforeAll() { //在所有 Test Case 執行之前先備份當前要用到的 Database Tables, //例如 export 成 XLS 檔案 try { URL testDatasetResourceFolderUrl = DBUnitTest.class.getClassLoader().getResource(testResourceFolderPath); File testDatasetResourceFolder = Paths.get(testDatasetResourceFolderUrl.toURI()).toFile(); backupDatasetResourceFilePath = testDatasetResourceFolder.getAbsolutePath() + backupDatasetResourcePath.replace("/", File.separator); testDatasetResourceFilePath = testDatasetResourceFolder.getAbsolutePath() + testDatasetResourcePath.replace("/", File.separator); IDatabaseConnection iDatabaseConnection = getIDatabaseConnection(); // partial database export, 只 export 部份 Table QueryDataSet partialDataSet = new QueryDataSet(iDatabaseConnection); partialDataSet.addTable("xxxTable", "SELECT * FROM xxxTable"); //輸出 XLS (Excel 97-2003 的格式)格式的檔案,可以用 Excel 開起來 XlsDataSet.write(partialDataSet, new FileOutputStream(backupDatasetResourceFilePath)); //也可以 Export XML 格式的檔案 //FlatXmlDataSet.write(partialDataSet, new FileOutputStream(backupDatasetResourceFilePath)); // full database export, 全部 Table 都 export //IDataSet fullDataSet = connection.createDataSet(); //XlsDataSet.write(fullDataSet, new FileOutputStream(backupDatasetResourceFilePath)); // dependent tables database export: export table X and all tables that // have a PK which is a FK on X, in the right order for insertion // Export 一個 Table 加上所有有指向這 Table 欄位外鍵的其他 Table //String[] depTableNames = TablesDependencyHelper.getAllDependentTables(connection, "xxxTable"); //IDataSet depDataSet = connection.createDataSet(depTableNames); //XlsDataSet.write(depDataSet, new FileOutputStream(backupDatasetResourceFilePath)); } catch (DatabaseUnitException | IOException | URISyntaxException e) { // TODO Auto-generated catch block e.printStackTrace(); } } @BeforeEach void beforeEach() { //在每一次的 Test Case 執行之前把 Test Dataset 檔案內容匯入當下 Database 中 try { IDatabaseConnection iDatabaseConnection = getIDatabaseConnection(); IDataSet dataSet = new XlsDataSet(new File(testDatasetResourceFilePath)); //XML 是用 FlatXmlDataSetBuilder().build() 取得 IDataSet //IDataSet dataSet = new FlatXmlDataSetBuilder().build(new File(testDatasetResourceFilePath)); //DatabaseOperation.CLEAN_INSERT 代表會先把 Database 相關 Table 清空再匯入 Test Dataset DatabaseOperation.CLEAN_INSERT.execute(iDatabaseConnection, dataSet); //你可以在這再執行一些需要的前置做業 Sql 語句, //例如使用 PostgreSQL 時,DatabaseOperation.CLEAN_INSERT.execute() 會把 //遞增欄位的遞增值重設成 1,造成之後測試 insert 時發生問題 (id 重覆等之類的), //這時就會需要在這做例如如下 Sql 語句來設置正確的遞增值: //SELECT setval('xxxTable_xxxField_seq', (SELECT coalesce(MAX(xxxField), 0) + 1 FROM xxxTable), false) } catch (DatabaseUnitException | SQLException | IOException e) { e.printStackTrace(); } } @AfterEach void afterEach() { } @AfterAll static void afterAll() { //在所有 Test Case 測試完後,用之前 Export 出來的 Backup Dataset 匯回去到 Database 中 try { IDatabaseConnection iDatabaseConnection = getIDatabaseConnection(); IDataSet dataSet = new XlsDataSet(new File(backupDatasetResourceFilePath)); DatabaseOperation.CLEAN_INSERT.execute(iDatabaseConnection, dataSet); //你可以在這再執行一些需要的前置做業 Sql 語句 } catch (DatabaseUnitException | SQLException | IOException e) { e.printStackTrace(); } } @Test void test() { //做你想做的 Test } }
我實測的情況是,目前 DBUnit 所產生的 XML Dataset 有一些問題,
例如如果 Dtabase 內容裡有紀錄包含對 XML 來說是 Invalid Character 的特殊字元的話,
例如:
- \v :垂直定位(\u000B,在 NotePad++ 中可以用 \013 配合正規表示法的搜尋找到)
- 一些使用到 Surrogate Pair 的 Emoji (可參考這篇 Code Point, Surrogate Pair 和 HTML Unicode 編碼),例如:👍 (Java 用 "\uD83D\uDC4D" 表示、HTML 用 👍 表示)
參考到這篇文章 在使用dbunit导出数据到Excel中时遇到的问题 - xm3530 - 博客园 也有一樣的狀況,文章中說要自行導入缺失的 lib ,最後需要引入 4.1.1 (4.1.2 好像也可以)的 poi Dependency 解決。
題外話我有找到一個以 DBUnit 為基礎的 Database Rider (官網、Github 專案) 專案,
它整合了 DBUnit 和 JUnit ,支援 JUnit 5,並且支持了產出其也格式的 Dataset,
例如: XML, XLS, XLSX, JSON, YML (實測 JSON 也會有例如 \v 特殊字元等的問題,而 YML 如果資料太大會有產出檔案大小的限制)。
我對 Database Rider 做了一些相關的使用紀錄,請參考這篇 使用 Dabase Rider 進行 Database 測試
參考資料:
沒有留言 :
張貼留言