2024年7月24日 星期三

使用 Dabase Rider 進行 Database 測試

上一篇 使用 DBUnit 進行 Database 測試 介紹了 DBUnit,
這篇要來介紹 Database Rider。

Database Rider 是一個基於 DBUnit 的專案,它擴展了 DBUnit 原有的功能,並整合了 JUnit 5,
增加了註解 (Annotation) 的使用方式,讓使用上更為便利,
並且也支援了更多的 Dataset 檔輸出格式,像是除了原有的 XML, XLS,也多了 XLSX, JSON, YML ,不過實測還是 XLS, XLSX 比較好用,YML, JSON 還是會有一些跟 DBUnit 一樣的特殊字元問題,例如無法正確處理 \v (\u000B) 等。

以下直接演示實際的程式碼範例:

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/com.github.database-rider/rider-junit5 -->
		<dependency>
		    <groupId>com.github.database-rider</groupId>
		    <artifactId>rider-junit5</artifactId>
		    <version>1.42.0</version>
		    <scope>test</scope>
		</dependency>
		
		<!-- Database Reider 輸出 XLSX Dataset 需要 poi-ooxml -->
		<!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml -->
		<dependency>
		    <groupId>org.apache.poi</groupId>
		    <artifactId>poi-ooxml</artifactId>
		    <version>5.3.0</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.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.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;
import org.junit.jupiter.api.extension.ExtendWith;

import com.github.database.rider.core.api.configuration.DBUnit;
import com.github.database.rider.core.api.connection.ConnectionHolder;
import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.core.api.dataset.DataSetFormat;
import com.github.database.rider.core.api.exporter.DataSetExportConfig;
import com.github.database.rider.core.exporter.DataSetExporter;
import com.github.database.rider.junit5.DBUnitExtension;

//可以不使用 @ExtendWith, @DBUnit Annotation, @DBRider,直接使用 Database Rider 提供的 API 來做,
//只有想要用例如 @DataSet, @ExpectedDataSet 這類 Annotation 的功能時才需要。
@ExtendWith(DBUnitExtension.class)
@DBUnit(escapePattern = "\"?\"",  //@DBUnit 可以設定一些 DBUnitConfig 參數給 @Dataset 使用
	    caseSensitiveTableNames = true,
	    allowEmptyFields = true)
//@DBRider(dataSourceBeanName = "xxx_DataSource") //如果有使用 Spring,可能會需要指定 Spring 中設定的 Data Source Name,因為 Database-Rider 會偵測到有 Spring 去抓 Datasource
class DBRiderTest {

	static final String testResourceFolderPath = "";
	static final String backupDatasetResourcePath = "/backupDataset.xlsx";
	static final String testDatasetResourcePath = "/testDataset.xlsx";
	
	static final String jdbcDriverName = "org.postgresql.Driver";
	static final String databaseUrl = "jdbc:postgresql://localhost:5432/xxxDatabase";
	static final String databaseUsername = "username";
	static final String databasePassword = "password";
	
	static final String[] includeTableList = new String[] {"xxxTable1", 
														   "xxxTable2", 
														   "xxxTable3"};
	
	//在使用 Annotation 的方式,即使用了 @ExtendWith(DBUnitExtension.class) 和 @DBUnit
	//或 @DBRider 時,可以定義一個 ConnectionHolder connectionHolder 屬性,
	//Database Rider 會用 Java Reflection (反射) 的方式從 connectionHolder
	//中呼叫 getConnection()來得到 Connection
	private static ConnectionHolder connectionHolder = new ConnectionHolder() {

		@Override
		public Connection getConnection() throws SQLException {
			try {
				Class.forName(jdbcDriverName);
			} catch (ClassNotFoundException e) {
				e.printStackTrace();
			}
			
			return DriverManager.getConnection(databaseUrl, databaseUsername, databasePassword);
		}
	};

	@BeforeAll //雖然 Database Rider 有提供 @ExportDataSet,但實測只有在被標注 @Test 的 non-static method 上才有用,不能用在 @BeforeAll 上,所以這裡直接使用 API 實作
	static void backupBackupDataset() {
		//在所有 Test Case 執行之前先備份當前要用到的 Database Tables,
		//例如 export 成 XLSX 檔案
		try {			
			URL backupDatasetResourceFolderUrl = DBRiderTest.class.getClassLoader().getResource(testResourceFolderPath);
			File backupDatasetResourceFolder = Paths.get(backupDatasetResourceFolderUrl.toURI()).toFile();
			String backupDatasetResourceFilePath = backupDatasetResourceFolder.getAbsolutePath() + backupDatasetResourcePath.replace("/", File.separator);
			
			DataSetExporter.getInstance().export(connectionHolder.getConnection(),
												 new DataSetExportConfig()
												 	 .dataSetFormat(DataSetFormat.XLSX)
													 //可以設定只要 Export 某些 Table
												 	 .includeTables(includeTableList)
												 	 .queryList(new String[] {})
												 	 .outputFileName(backupDatasetResourceFilePath));
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	@BeforeEach
	//在每一次的 Test Case 執行之前把 Test Dataset 檔案內容匯入當下 Database 中    
	@DataSet(value = testResourceFolderPath + testDatasetResourcePath,
             //executeStatementsBefore 可以設定在塞 dataset 前想要執行的 sql 語句,
             //例如 SQL Server (MS Sql) 要塞 Primary Key 值時可能會需要設定 SET IDENTITY_INSERT test_table ON 才行
             executeStatementsBefore = {},
             //executeStatementsAfter 實測好像沒效果,不確定是什麼原因還是是 Bug ?
             //所以如果有需要在 DataSet 塞入 Database 後執行的 Sql,
             //使用 @DataSet 的 Annotation (註解) 方式時,
             //還是必須在 method function 中直接用程式碼執行 DataSet 塞入 Database 後要執行的 Sql 語句,
             //例如 PostgreSQL 可能會需要在放入 dataset 後要設定遞增欄位的值,像是:
			 //SELECT setval('xxxTable_xxxField_seq', (SELECT coalesce(MAX(xxxField), 0) + 1 FROM xxxTable), false)
             executeStatementsAfter = {}
             )
	void importTestDatasetByAnnotation() throws DatabaseUnitException, SQLException {        
        try (Connection conn = connectionHolder.getConnection();
				 PreparedStatement pstmt = conn.prepareStatement("SELECT setval('xxxTable_xxxField_seq', (SELECT coalesce(MAX(xxxField), 0) + 1 FROM xxxTable), false)");) {
		
			try (ResultSet rs = pstmt.executeQuery();) {
			}
		} catch (SQLException e) {
			Assert.fail("Exception occured: " + getExceptionDetail(e));
		}
	}
    
    //也可以不使用 @DataSet,直接用 DatabaseRider 提供的 API 塞資料給 Database
    @BeforeEach 
	static void importTestDataset() throws DatabaseUnitException, SQLException {
		try (Connection conn = connectionHolder.getConnection();) {
        	RiderDSL.withConnection(conn)
			    	.withDataSetConfig(new DataSetConfig(testResourceFolderPath + testDatasetResourcePath))
			    	.withDBUnitConfig(new DBUnitConfig()
                                  	  .cacheConnection(false) //這個很重要,因為預設是 true,有可能會造成不預期的意外,例如使用了 cache 的 connection 但不知 connection 可能早被 close 了
					   			  	  .addDBUnitProperty("escapePattern", "\"?\"")
			    				  	  .addDBUnitProperty("caseSensitiveTableNames", true)
			    				  	  .addDBUnitProperty("allowEmptyFields", true))
			    	.createDataSet();
		}
        
		try (Connection conn = connectionHolder.getConnection();
				 PreparedStatement pstmt = conn.prepareStatement("SELECT setval('xxxTable_xxxField_seq', (SELECT coalesce(MAX(xxxField), 0) + 1 FROM xxxTable), false)");) {
		
			try (ResultSet rs = pstmt.executeQuery();) {
			}
		} catch (SQLException e) {
			Assert.fail("Exception occured: " + getExceptionDetail(e));
		}
	}
    
    

	@AfterEach
	void afterEach() {
	}

	@AfterAll //因為實測發現 @Dataset 只能作用在被標注 @Test 的 non-static method 上,所以這裡直接使用 API 實作
	static void importBackupDatasetBack() throws DatabaseUnitException, SQLException {
    	try (Connection conn = connectionHolder.getConnection();) {
			RiderDSL.withConnection(conn)
			    	.withDataSetConfig(new DataSetConfig(testResourceFolderPath + backupDatasetResourcePath))
			    	.withDBUnitConfig(new DBUnitConfig()
                				  	  .cacheConnection(false)
					   			  	  .addDBUnitProperty("escapePattern", "\"?\"")
			    				  	  .addDBUnitProperty("caseSensitiveTableNames", true)
			    				  	  .addDBUnitProperty("allowEmptyFields", true))
			    	.createDataSet();
		}
        
		try (Connection conn = connectionHolder.getConnection();
				 PreparedStatement pstmt = conn.prepareStatement("SELECT setval('xxxTable_xxxField_seq', (SELECT coalesce(MAX(xxxField), 0) + 1 FROM xxxTable), false)");) {
		
			try (ResultSet rs = pstmt.executeQuery();) {
			}
		} catch (SQLException e) {
			Assert.fail("Exception occured: " + getExceptionDetail(e));
		}
	}
	
	//使用 @ExpectedDataSet Annotation 比較 database table 的資料是否和 expected dataset 一樣
	@Test
	@ExpectedDataSet(value = "expectedDataset.xlsx")
	void testExpectedDatasetByAnnotation() throws DatabaseUnitException, SQLException {
		// 做你想做的 Test
		
		//例如以下是比較 Actual dataset 和 expected dataset 的範例 (請再自己準備一個 expected dataset file):
		//自己修改一下 database table 的資料
		try(PreparedStatement pstmt = connectionHolder.getConnection().prepareStatement("UPDATE test_table SET title = 'ABC' WHERE id = 2")) {
			pstmt.executeUpdate();
		}
		
		//因為使用了 @ExpectedDataSet Annotation,所以這裡就不需要再多寫用 RiderDSL 等的程式碼了
	}
	
	@Test //也可以不使用用 @ExpectedDataSet Annotation,直接使用 RiderDSL 來比較
	void testExpectedDataset() throws DatabaseUnitException, SQLException {
		// 做你想做的 Test
		
		//例如以下是比較 Actual dataset 和 expected dataset 的範例 (請再自己準備一個 expected dataset file):
		//自己修改一下 database table 的資料
		try(PreparedStatement pstmt = connectionHolder.getConnection().prepareStatement("UPDATE test_table SET title = 'ABC' WHERE id = 2")) {
			pstmt.executeUpdate();
		}
		
		//比較 database table 的資料是否和 expected dataset 一樣
		RiderDSL.withConnection(connectionHolder.getConnection())
			    .withDataSetConfig(new DataSetConfig("expectedDataset.xlsx"))
			    .withDBUnitConfig(new DBUnitConfig()
					              .addDBUnitProperty("escapePattern", "\"?\"")
					              .addDBUnitProperty("caseSensitiveTableNames", true)
					              .addDBUnitProperty("allowEmptyFields", true))
		        .expectDataSet(new ExpectedDataSetConfig());
	}
}

參考資料:

  1. Database Rider
  2. GitHub - database-rider/database-rider: Database testing made easy!

沒有留言 :

張貼留言