顯示具有 TestNg 標籤的文章。 顯示所有文章
顯示具有 TestNg 標籤的文章。 顯示所有文章

2024年7月29日 星期一

使用 JUnit5 Runner + TestNg + Dabase Rider 進行 Database 測試

這篇要來紀錄使用 JUnit5 Runner + TestNg + Dabase Rider 進行 Database 測試的方法。

首先,用 JUnit5 來跑 TestNg 的方法可以參考這篇 使用 JUnit5 同時跑 JUnit5 和 TestNg 的 Test Case,只要使用 testng-engine 就可以了。

再來,因為 Database Rider (可以先參考之前的 使用 DBUnit 進行 Database 測試 和 使用 Dabase Rider 進行 Database 測試 這兩篇) 的 Annotation (註解,例如 @DBUnit、@Dataset 這些) 是用 @ExtendWith(DBUnitExtension.class) 的方式以 Extension 來跟 JUnit5協作,
但 TestNg 是看不懂這些 Database Rider 的 Annotation 的,
所以我們要用如 DBUnit 最普通的方式那樣,
自己用程式碼執行的方式 (就是不用 Annotation 來進行 DataSet 的建立、寫資料進 Database 等操作。

下面直接示範程式碼:

Maven Dependency 的 pom.xml:

<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>junittest</artifactId>
	<version>0.0.1-SNAPSHOT</version>

	<properties>
		<maven.compiler.target>11</maven.compiler.target>
		<maven.compiler.source>11</maven.compiler.source>
	</properties>

	<build>
		<plugins>
			<!-- Maven 執行 test 的 plugin,surefire 多用於單元測試 (unit test), failsafe
			多用於整合測試 (integration test)  -->
			<plugin>
				<artifactId>maven-surefire-plugin</artifactId>
				<version>3.1.2</version>
			</plugin>
			<plugin>
				<artifactId>maven-failsafe-plugin</artifactId>
				<version>3.1.2</version>
			</plugin>
		</plugins>
	</build>

	<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.testng/testng -->
		<dependency>
			<groupId>org.testng</groupId>
			<artifactId>testng</artifactId>
			<version>7.10.2</version>
			<scope>test</scope>
		</dependency>

		<!-- 使用 testng-engine 讓 JUnit5 可以跑 TestNg 的 Test Case -->
		<!-- https://mvnrepository.com/artifact/org.junit.support/testng-engine -->
		<dependency>
			<groupId>org.junit.support</groupId>
			<artifactId>testng-engine</artifactId>
			<version>1.0.5</version>
			<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>

	</dependencies>

</project>

TestNgDatabaseRiderTest.java:

package junittest;

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.testng.annotations.AfterTest;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import com.github.database.rider.core.api.dataset.DataSetFormat;
import com.github.database.rider.core.api.exporter.DataSetExportConfig;
import com.github.database.rider.core.configuration.DBUnitConfig;
import com.github.database.rider.core.configuration.DataSetConfig;
import com.github.database.rider.core.dsl.RiderDSL;
import com.github.database.rider.core.exporter.DataSetExporter;

public class TestNgDatabaseRiderTest {

	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"};
                                                           
    static String[] statementsForExecuteAfterDataSet = new String[]{"SELECT setval('xxxTable1_xxxField_seq', (SELECT coalesce(MAX(xxxField), 0) + 1 FROM xxxTable1), false)",
            	   									   		        "SELECT setval('xxxTable2_xxxField_seq', (SELECT coalesce(MAX(xxxField), 0) + 1 FROM xxxTable2), false)"};

	@BeforeTest
	static void beforeAll() {
		//在所有 Test Case 執行之前先備份當前要用到的 Database Tables,
		//例如 export 成 XLSX 檔案
		try {			
			URL backupDatasetResourceFolderUrl = TestNgDatabaseRiderTest.class.getClassLoader().getResource(testResourceFolderPath);
			File backupDatasetResourceFolder = Paths.get(backupDatasetResourceFolderUrl.toURI()).toFile();
			String backupDatasetResourceFilePath = backupDatasetResourceFolder.getAbsolutePath() + backupDatasetResourcePath.replace("/", File.separator);
			
			DataSetExporter.getInstance().export(getConnection(),
												 new DataSetExportConfig()
												 	 .dataSetFormat(DataSetFormat.XLSX)
												 	 //可以設定只要 Export 某些 Table
												 	 .includeTables(includeTableList)
												 	 .queryList(new String[] {})
												 	 .outputFileName(backupDatasetResourceFilePath));
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	@BeforeMethod
	//在每一次的 Test Case 執行之前把 Test Dataset 檔案內容匯入當下 Database 中
	void beforeEach() throws DatabaseUnitException, SQLException {
		System.out.println("before each");
		
        try (Connection conn = getConnection();) {
			RiderDSL.withConnection(conn)
			    	.withDataSetConfig(new DataSetConfig(testResourceFolderPath + testDatasetResourcePath)				    		       
			    				   	//實測 executeStatementsBefore 和 executeStatementsAfter 效果不好,
                                   	// executeStatementsBefore 會在 DataSet 塞入 Database 前被執行,但我想要在 DataSet 塞入 Database 後執行。
                                   	// executeStatementsAfter 我目前測不出來什麼時候才會被執行,可能我理解有誤?
                                   	//.executeStatementsBefore("")
                                   	//.executeStatementsAfter("")
                                   	)
			    	.withDBUnitConfig(new DBUnitConfig()
                    				 .cacheConnection(false) //這個很重要,因為預設是 true,有可能會造成不預期的意外,例如使用了 cache 的 connection 但不知 connection 可能早被 close 了
					   			  	 .addDBUnitProperty("escapePattern", "\"?\"")
			    				  	 .addDBUnitProperty("caseSensitiveTableNames", true)
			    				  	 .addDBUnitProperty("allowEmptyFields", true))
	    	.createDataSet();
        }
        
        //你可以在這指定一些需要執行的前置做業 Sql 語句給 executeStatementsBefore,
        //例如使用 PostgreSQL 時,DatabaseOperation.CLEAN_INSERT.execute() 會把
        //遞增欄位的遞增值重設成 1,造成之後測試 insert 時發生問題 (id 重覆等之類的),
		//這時就會需要在這做例如如下 Sql 語句來設置正確的遞增值:
		//SELECT setval('xxxTable_xxxField_seq', (SELECT coalesce(MAX(xxxField), 0) + 1 FROM xxxTable), false)
        for (String statement : statementsForExecuteAfterDataSet) {
			try (Connection conn = getConnection();
				 PreparedStatement pstmt = conn.prepareStatement(statement);) {

				try (ResultSet rs = pstmt.executeQuery();) {
				}
			} catch (SQLException e) {
				Assert.fail("Exception occured: " + getExceptionDetail(e));
			}
		}
	}

	@AfterMethod
	void afterEach() {
	}

	@AfterTest
	//在所有 Test Case 測試完後,用之前 Export 出來的 Backup Dataset 匯回去到 Database 中
	static void afterAll() throws DatabaseUnitException, SQLException {
		System.out.println("afterAll");
		
        try (Connection conn = getConnection();) {
			RiderDSL.withConnection(conn)
        	    	.withDataSetConfig(new DataSetConfig(testResourceFolderPath + backupDatasetResourcePath)
        	    				   		.executeStatementsBefore("SELECT setval('xxxTable1_xxxField_seq', (SELECT coalesce(MAX(xxxField), 0) + 1 FROM xxxTable1), false)",
			    						   						 "SELECT setval('xxxTable2_xxxField_seq', (SELECT coalesce(MAX(xxxField), 0) + 1 FROM xxxTable2), false)"))
        	    	.withDBUnitConfig(new DBUnitConfig()
                			      	  .cacheConnection(false) //這個很重要,因為預設是 true,有可能會造成不預期的意外,例如使用了 cache 的 connection 但不知 connection 可能早被 close 了
        			   			  	  .addDBUnitProperty("escapePattern", "\"?\"")
        	    				  	  .addDBUnitProperty("caseSensitiveTableNames", true)
        	    				  	  .addDBUnitProperty("allowEmptyFields", true))
        	    	.createDataSet();
        }
                
        for (String statement : statementsForExecuteAfterDataSet) {
			try (Connection conn = getConnection();
				 PreparedStatement pstmt = conn.prepareStatement(statement);) {

				try (ResultSet rs = pstmt.executeQuery();) {
				}
			} catch (SQLException e) {
				Assert.fail("Exception occured: " + getExceptionDetail(e));
			}
		}        
	}
	
    //TestNg 可以使用 dependsOnMethods 來設定要在哪個 Test methods 之後才執行
	@Test(dependsOnMethods = {"test2"})
	void test() {
		assertEquals(1, 1, "Should be equal.");
		System.out.println("TestNg: test1");
	}
	
	@Test
	void test2() {
		assertEquals(1, 1, "Should be equal.");
		System.out.println("TestNg: test2");
	}
	
	static Connection getConnection() {
		Connection connection = null;
		
		try {
			Class.forName(jdbcDriverName);
			connection = DriverManager.getConnection(databaseUrl, databaseUsername, databasePassword);
		} catch (ClassNotFoundException | SQLException e) {
			e.printStackTrace();
		}
		
		return connection;
	}
}

參考資料:

  1. DataSet Executor
  2. RiderDSL

2024年7月28日 星期日

使用 JUnit5 同時跑 JUnit5 和 TestNg 的 Test Case

通常來講 Java 的 Unit Test 只會用一個 Framework 來跑,
例如使用JUnit5、或著是使用 TestNg 等,一次只會用一種 Framework,
不過 JUnit5 提供一個 JUnit Platform 可供其他第三方的 Unit Test framework去 implement,
讓 JUnit5 可以跑其他的 Unit Test framework,
這樣我們就可以同時跑專案裡的 JUnit5 和 TestNg 的測試案例了。

下面展示範例:

Maven Dependency 的 pom.xml:

<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>junittest</artifactId>
	<version>0.0.1-SNAPSHOT</version>

	<properties>
		<maven.compiler.target>11</maven.compiler.target>
		<maven.compiler.source>11</maven.compiler.source>
	</properties>

	<build>
		<plugins>
			<plugin>
				<artifactId>maven-surefire-plugin</artifactId>
				<version>3.1.2</version>
			</plugin>
			<plugin>
				<artifactId>maven-failsafe-plugin</artifactId>
				<version>3.1.2</version>
			</plugin>
		</plugins>
	</build>

	<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.testng/testng -->
		<dependency>
			<groupId>org.testng</groupId>
			<artifactId>testng</artifactId>
			<version>7.10.2</version>
			<scope>test</scope>
		</dependency>

		<!-- 使用 testng-engine 讓 JUnit5 可以跑 TestNg 的 Test Case -->
		<!-- https://mvnrepository.com/artifact/org.junit.support/testng-engine -->
		<dependency>
			<groupId>org.junit.support</groupId>
			<artifactId>testng-engine</artifactId>
			<version>1.0.5</version>
			<scope>test</scope>
		</dependency>

	</dependencies>

</project>

兩個 Test Case Class,一個使用 JUnit5、另一個使用 TestNg:

Juit5Test.java (使用 JUnit5):

package junittest;

import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

public class Juit5Test {
	
	@BeforeAll
	static void beforeAll() {
		System.out.println("Junit5: beforeAll");
	}
	
	@Test
	void test() {
		assertEquals(1, 1, "Should be equal.");
		System.out.println("Junit5: test1");
	}
	
	@Test
	void test2() {
		assertEquals(1, 1, "Should be equal.");
		System.out.println("Junit5: test2");
	}
}

TestNgTest.java (使用 TestNg):

package junittest;

import static org.testng.Assert.assertEquals;

import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;
public class TestNgTest{

	@BeforeTest
	static void beforeAll() {
		System.out.println("TestNg: beforeAll");
	}
	
	//TestNg 可以使用 dependsOnMethods 來設定要在哪個 Test methods 之後才執行
	@Test(dependsOnMethods = {"test2"})
	void test() {
		assertEquals(1, 1, "Should be equal.");
		System.out.println("TestNg: test1");
	}
	
	@Test
	void test2() {
		assertEquals(1, 1, "Should be equal.");
		System.out.println("TestNg: test2");
	}
	
}

接下來我們就可以使用 Maven 的 test 指令來跑 Test Case:

mvn clean test

以下是跑完的結果,可以看到 JUnit5 和 TestNg 的 Test Case 都有被成功執行

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
[INFO] Running junittest.Juit5Test
Junit5: beforeAll
Junit5: Junit5: OK1
Junit5: Junit5: OK2
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.028 s -- in junittest.Juit5Test
[INFO] Running junittest.TestNgTest
TestNg: beforeAll
TestNg: TestNg: OK2
TestNg: TestNg: OK1
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.005 s -- in junittest.TestNgTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0

The engine’s main intention is integration with build tools like Gradle and Maven. Hence, custom suites specified via testng.xml files are not supported.

  1. TestNG Engine for the JUnit Platform
  2. JUnit Jupiter Episode 3 - Running JUnit 4 / JUnit 5 / TestNG Tests in a Maven project.
  3. TestNg and Junit not running in same gradle project