2024年12月4日 星期三

Javascript - 使用 Array.splice() 交換 List 中的兩 Item 的位置

紀錄一下使用 Javascript 的 Array.splice() 交換 List 中的兩個 Item 位置的方法,
例如現在想要交換 list 中 index1 和 index2 位置的 item ,
可以如下達成:

var list = [1,2,3,4]; //list = [1,2,3,4]
var index1 = 0;
var index2 = 2;
list[index1] = list.splice(index2, 1, list[index1])[0]; //list = [3,2,1,4]

2024年8月28日 星期三

Spring - 使用 MockMVC 做 Controller 的 Unit Test - MockMvcBuilders.webAppContextSetup(webApplicationContext ) 與 MockMvcBuilders.standaloneSetup(controller) 的差別

在 SpringMVC Project中,如果想要對 Controller 做 Unit Test 的話,

通常會使用 Spring 提供的 MockMvc 來達成,
獲取了 MockMvc 後,其提供了模擬 HttpPost, HttpGet 等 HttpRequest 去觸發 Controller 中的 Method 以供測試,範例程式碼如下:  

MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/xxx/xxx/xxx.do")
									  .session(new MockHttpSession())
									  .param("xxx", "yyy")
									  ).andReturn();
String responseBody = mvcResult.getResponse().getContentAsString();

Spring 提供了兩個 Method 供我們取得 MockMvc ,分別是: MockMvcBuilders.webAppContextSetup(webApplicationContext)

MockMvcBuilders.standaloneSetup(controller)。

MockMvcBuilders.webAppContextSetup(webApplicationContext) 需要的參數是 webApplicationContext,
webApplicationContext 可以用指定 Spring Config XML file 、指令 Config Class 、自己手動獲取 (例如自己使用 new XmlWebApplicationContext() 或 new AnnotationConfigWebApplicationContext() 方式做設定取得) 來取得,
此時 Spring 會幫我們建立整個 Spring Application 的上下文 (Context)。
如果使用 Junit 5 , Spring 提供了一個 SpringExtension  的 Junit 5 Extension 可以使用,
它可以幫我們建立 Spring Application 上下文,並讓我們可以在 Unit Test 中使用 @Autowired 取得裝配好的 Bean。
因為會建立整個 Spring Application ,所以會花比較久的時間,
並且如果想要用 Mock 去取代某些依賴 Bean 的話 (例如 Controller 裡面用 @Autowired 設定的特定 Service),需要自己用 ReflectionTestUtils 以反射(Reflection)去處理。
因為整個 Spring Application 的上下文都被建立起來,Spring Bean 都被裝配完成的關係,
此方法比較接近整合測試 (Integration Test) 而不像單元測試 (Unit Test)。

MockMvcBuilders.standaloneSetup(controller) 只需要特定待測的 Controller (可以多個) 做為參數,
此時 Spring 只會以做為參數的 Controller 來建立 mockMvc,
因為此方法不會設定 Spring Config,所以 Spring 也不會知道如何裝配 Controller 中的依賴,
裝配依賴要自己實做,
我們可以使用 Mockito 提供的 @Mock 和 @Injects 註解來實現 Mock 依賴裝配,
被 @Mock 標注的 Bean 能以 Mock Bean 的方式 Inject 進被 @Injects 標注的 Bean 中。
此外,因為不用建立整個 Spring Application 的關係,此方式執行的速度比較快。
也因為只關注待測的 Controller ,其他的都是 Mock Bean,所以比較接近單元測試 (Unit Test)。

下面展示一下程式範例,直接看程式應該會比較清楚了解,相關的說明也已寫在了註解中。

這裡範例借用了上一篇 No XML for Java EE Spring Application 的設定,
順便複習一下使用 No XML 的方式來設定 Java EE Spring Application,
所以此例不存在 web.xml 和 Spring Config File。

專案結構如下圖:


pom.xml :
<?xml version="1.0" encoding="UTF-8"?>

<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>test</groupId>
	<artifactId>springunittest</artifactId>
	<version>0.1</version>
	<packaging>war</packaging>

	<name>springunittest Maven Webapp</name>
	<!-- FIXME change it to the project's website -->
	<url>http://www.example.com</url>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<maven.compiler.source>11</maven.compiler.source>
		<maven.compiler.target>11</maven.compiler.target>
	</properties>

	<build>
		<finalName>springunittest</finalName>
		<pluginManagement><!-- lock down plugins versions to avoid using Maven
			defaults (may be moved to parent pom) -->
			<plugins>
				<plugin>
					<artifactId>maven-clean-plugin</artifactId>
					<version>3.1.0</version>
				</plugin>
				<!-- see
				http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
				<plugin>
					<artifactId>maven-resources-plugin</artifactId>
					<version>3.0.2</version>
				</plugin>
				<plugin>
					<artifactId>maven-compiler-plugin</artifactId>
					<version>3.8.0</version>
				</plugin>
				<plugin>
					<artifactId>maven-surefire-plugin</artifactId>
					<version>3.1.2</version>
				</plugin>
				<plugin>
					<artifactId>maven-failsafe-plugin</artifactId>
					<version>3.1.2</version>
				</plugin>
				<plugin>
					<artifactId>maven-war-plugin</artifactId>
					<version>3.2.2</version>
				</plugin>
				<plugin>
					<artifactId>maven-install-plugin</artifactId>
					<version>2.5.2</version>
				</plugin>
				<plugin>
					<artifactId>maven-deploy-plugin</artifactId>
					<version>2.8.2</version>
				</plugin>
			</plugins>
		</pluginManagement>
	</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.mockito/mockito-junit-jupiter -->
		<dependency>
			<groupId>org.mockito</groupId>
			<artifactId>mockito-junit-jupiter</artifactId>
			<version>5.10.0</version>
			<scope>test</scope>
		</dependency>

		<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>javax.servlet-api</artifactId>
			<version>4.0.1</version>
		</dependency>

		<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-webmvc</artifactId>
			<version>5.3.18</version>
		</dependency>

		<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-jdbc</artifactId>
			<version>5.3.18</version>
		</dependency>
		
		<!-- https://mvnrepository.com/artifact/org.springframework/spring-test -->
		<dependency>
		    <groupId>org.springframework</groupId>
		    <artifactId>spring-test</artifactId>
		    <version>5.3.18</version>
		    <scope>test</scope>
		</dependency>
	</dependencies>
</project>

src/main/java/com/config/WebInitializer.java :

package com.config;

import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;

import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.context.support.XmlWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

//Servlet Container (例如 Tomcat) 會在 Web 應用啟動時尋找實作 WebApplicationInitializer 的 Class 並執行 onStartup()
public class WebInitializer implements WebApplicationInitializer {
	
	@Override
	public void onStartup(ServletContext servletContext) {
		
		//可以用 AnnotationConfigWebApplicationContext 設定 Class Config
		//或是用 XmlWebApplicationContext 設定 XML Config 
		//來建立 Spring Application 上下文 (Context)
		AnnotationConfigWebApplicationContext springWebApplicationContext = new AnnotationConfigWebApplicationContext();
		springWebApplicationContext.register(SpringApplicationConfig.class); //這裡可以指定 Spring Class Config
//		XmlWebApplicationContext springWebApplicationContext = new XmlWebApplicationContext();
//		springWebApplicationContext.setConfigLocation("classpath:application-config.xml"); //這裡可以指定 Spring XML Config
		
		DispatcherServlet dispatcherServlet = new DispatcherServlet(springWebApplicationContext);
		
		ServletRegistration.Dynamic springDispatcherServlet = servletContext.addServlet("springDispatcherServlet", dispatcherServlet);
		springDispatcherServlet.setLoadOnStartup(1);
		springDispatcherServlet.addMapping("/");
	}
}

src/main/java/com/config/SpringApplicationConfig.java :

package com.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@EnableWebMvc
@Configuration
@ComponentScan(basePackages = "com.*")
public class SpringApplicationConfig {
	
	@Bean
	public InternalResourceViewResolver setupViewResolver() {
		InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
		viewResolver.setPrefix("/WEB-INF/view/");
		viewResolver.setSuffix(".jsp");
		
		return viewResolver;
	}
}

src/main/java/com/controller/TestController.java :

package com.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.service.TestService;

@Controller
public class TestController {
	
	@Autowired
	TestService testService;
	
	@ResponseBody
	@RequestMapping("/test")
	public String test() {
		return "HIHI";
	}
	
	@RequestMapping("/hello")
	public String hello() {
		return "hello";
	}
	
	@ResponseBody
	@RequestMapping("/grettingWithNameByMemberId")
	public String grettingWithNameByMemberId(int memberId) {
		return "Hello, " + testService.getNameByMemberId(memberId);
	}
	
}

src/main/java/com/service/TestService.java :

package com.service;

import org.springframework.stereotype.Service;

@Service
public class TestService {

	public String getNameByMemberId(int memberId) {
		return "realName";
	}
}

src/test/java/test/ControllerTestOnlySpecificController.java :

package test;

import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.mockito.ArgumentMatchers.anyInt;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import com.controller.TestController;
import com.service.TestService;

//預設 Junit 會為每個測試方法建立新的 Class Instance 來避免各測試方法間互相影響,
//所以 @BeforeAll 和 @AfterAll 只能標注在 static method 上。
//如果有需求需要把 @BeforeAll, @AfterAll 設定在 non-static method 上的話,
//可以設定 Lifecycle.PER_CLASS (預設是 Lifecycle.PER_METHOD),此時 JUnit 會改成只建立一次 Class Instance,
//並在同一個 Class Instance 中執行測試方法,
//這樣我們就可以把 @BeforeAll, @AfterAll 標注在 non-static method 上 
@TestInstance(Lifecycle.PER_CLASS) //這裡只是舉例紀錄一下用法
public class ControllerTestOnlySpecificController {

	AutoCloseable closeableMocks;
	
	MockMvc mockMvc;
	
	//被標注 @InjectMocks 的 Bean 中,
	//如果有用到 Spring 裝配的 Bean,
	//被 @Mock 標注的 Bean 會被 Inject 到被標注 @InjectMocks 的 Bean 中。
	//例如此例的 TestController 有用到 TestService,
	//因為 TestController 被標注 @InjectMocks 和
	//TestService 被標注了 @Mock,
	//所以 TestService 會被 Inject 到 TestController 中
	@InjectMocks
	TestController mockTestController;
	
	@Mock
	TestService mockTestService;
	
	@BeforeAll
	void beforeAll() {
	}
	
	@AfterAll
	void afterAll() throws Exception {
	}
	
	@BeforeEach
	void beforeEach() {
		//使用 MockitoAnnotations.openMocks() 來讓 @InjectMocks, @Mock 等註解產生作用
		closeableMocks = MockitoAnnotations.openMocks(this);
		//這裡沒有讓 Spring 建立 Spring Application 上下文 (Context),所以也不用指定 Spring Config,
		//這裡只有讓 Spring 根據特定 Controller(可以指定多個) 建立只為測試 Controller 的 mockMvc,
		//必需自行設定 Controller 中會用到的所有依賴行為,例如此例的 TestService,
		//因為沒有建立整個 Spring Application,所以執行速度比較快,
		//且因為 Controller 中的所有相關依賴都沒有被 Spring 裝配起來,只關注 Controller 功能本身,
		//所以比較偏向 Unit Test 而不是 Integration Test 
		mockMvc = MockMvcBuilders.standaloneSetup(mockTestController).build();
	}
	
	@AfterEach
	void afterEach() throws Exception {
		closeableMocks.close();
	}
	
	@Test
	void grettingWithNameByMemberIdTest() throws Exception {
		//可以用 MockHttpSession 模擬 Session
		MockHttpSession mockSession = new MockHttpSession();
		
		String mockName = "mockName";
		String expectedReturnString = "Hello, " + mockName;
		//可以設宣 mockTestService.querySomeDataById(int id) 要回傳什麼值
		//注意:如果參數是 int,不能用 any() 代替,因為 int 不是 Object,此時 any() 會判斷用 null 回傳,
		//而造成 null 無法被轉型成 int 造成 NullPointerException
		when(mockTestService.getNameByMemberId(anyInt())).thenReturn(mockName);
		//使用 MockMvc 樣擬 Http Request 觸發 Controller,此例模擬 Http Post
		MvcResult mvcResult = mockMvc.perform(post("/grettingWithNameByMemberId")
										      .session(mockSession)
										      .param("memberId", "12345")
										     ).andReturn();
		String responseBody = mvcResult.getResponse().getContentAsString();
		Assertions.assertEquals(expectedReturnString, responseBody, "Should get correct data.");
	}
}

src/test/java/test/ControllerTestRunningWholeSpringApplication.java :

package test;

import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.mockito.ArgumentMatchers.anyInt;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import com.config.SpringApplicationConfig;
import com.controller.TestController;
import com.service.TestService;

//使用以下設定後,Spring 會幫我們以設定的 Spring Config 建立完整的 Spring Application 上下文 (Context),
//我們就可以得到實際裝配好的 Spring Bean。
//嚴格說這比較像是整合測試(Integration Test)而不是單元測試(Unit test),
//單元測試應該要只測試 Controller 本身,其中依賴的其他 Bean (例如 TestService) 不應該由 Spring 幫忙裝配
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration(classes = SpringApplicationConfig.class)
//也可以指定 Spring XML 的位置
//@ContextConfiguration({"file:xxx/xxx/spring-config.xml"})
/**
 * @ExtendWith + @WebAppConfiguration + @ContextConfiguration = @SpringJUnitWebConfig
 * 上述程式碼等同於:
 * @SpringJUnitWebConfig(SpringApplicationConfig.class)
 */
@TestInstance(Lifecycle.PER_CLASS)
public class ControllerTestRunningWholeSpringApplication {

AutoCloseable closeableMocks;
	
	MockMvc mockMvc;
	
	@Autowired
	TestController testController;
	
	@Autowired
	TestService testService;

	@BeforeAll
	void beforeAll(WebApplicationContext webApplicationContext) {
		//webApplicationContext 也可以用 @Autowired 得到
		mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
	}
	
	@BeforeEach
	void beforeEach() {
		closeableMocks = MockitoAnnotations.openMocks(this);
	}
	
	@AfterEach
	void afterEach() throws Exception {
		closeableMocks.close();
	}
	
	@Test
	void grettingWithNameByMemberIdTest() throws Exception {
		MockHttpSession mockSession = new MockHttpSession();
		
		String mockName = "mockName";
		String expectedReturnString = "Hello, " + mockName;
		//如果要對特定 Controller 的依賴用 Mock 替換來測試,例如 TestService,
		//可以用 ReflectionTestUtils 使用反射(Reflection) 來達成,
		//測完後別忘了把原來的依賴換回去,不要影響其他他測試方法。
		TestService mockTestService = Mockito.mock(TestService.class);
		ReflectionTestUtils.setField(testController, "testService", mockTestService);
		when(mockTestService.getNameByMemberId(anyInt())).thenReturn(mockName);
		
		//使用 MockMvc 樣擬 Http Request 觸發 Controller,此例模擬 Http Post
		MvcResult mvcResult = mockMvc.perform(post("/grettingWithNameByMemberId")
										      .session(mockSession)
										      .param("memberId", "12345")
										     ).andReturn();
		String responseBody = mvcResult.getResponse().getContentAsString();
		Assertions.assertEquals(expectedReturnString, responseBody, "Should get correct data.");

		//把 TestController 原來的依賴換回去,不要影響其他他測試方法。
		ReflectionTestUtils.setField(testController, "testService", testService);
	}
}

源碼下載分享:
spring-unit-test.7z

參考資料:

  1. 第 5 章 Spring 应用的测试 - 《Java 研发自测》
  2. Setup Choices :: Spring Framework
  3. 测试实例生命周期 | Junit 5官方文档中文版
  4. 菜鳥工程師 肉豬: Mockito @Mock與@InjectMocks的差別
  5. The SpringJUnitConfig and SpringJUnitWebConfig Annotations in Spring 5 – Spring5中的SpringJUnitConfig和SpringJUnitWebConfig注解

2024年8月27日 星期二

No XML for Java EE Spring Application

這裡來紀錄一下如何不使用 XML 配置檔改用程式的方式來設定 Java EE + Spring 的 Web Application。

首先來看一下專案的檔案結構如下圖:




因為不用 XML 了的關係,所以我們這次範例專案中不會特別放 Java EE 的 web.xml 和 Spring 的 Config XML。

pom.xml (設定了一些要用到的 Dependency) :
<?xml version="1.0" encoding="UTF-8"?>

<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>my.test</groupId>
  <artifactId>javaee-spring-no-xml-test</artifactId>
  <version>0.1</version>
  <packaging>war</packaging>

  <name>javaee-spring-no-xml-test Maven Webapp</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    
    <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
	<dependency>
	    <groupId>javax.servlet</groupId>
	    <artifactId>javax.servlet-api</artifactId>
	    <version>4.0.1</version>
	</dependency>
	
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->  
    <dependency>
    	<groupId>org.springframework</groupId>
    	<artifactId>spring-webmvc</artifactId>
    	<version>5.3.18</version>
	</dependency>
	
	<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
	<dependency>
	    <groupId>org.springframework</groupId>
	    <artifactId>spring-jdbc</artifactId>
	    <version>5.3.18</version>
	</dependency>

  </dependencies>

  <build>
    <finalName>javaee-spring-no-xml-test</finalName>
    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
      <plugins>
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.1.0</version>
        </plugin>
        <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.22.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-war-plugin</artifactId>
          <version>3.2.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>2.5.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>2.8.2</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>

src/main/com/config/WebInitializer.java (Java EE Web Application 的啟動設定,取代 web.xml) :
package com.config;

import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;

import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.context.support.XmlWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

//Servlet Container (例如 Tomcat) 會在 Web 應用啟動時尋找實作 WebApplicationInitializer 的 Class 並執行 onStartup()
public class WebInitializer implements WebApplicationInitializer {
	
	@Override
	public void onStartup(ServletContext servletContext) {
		
		//可以用 AnnotationConfigWebApplicationContext 設定 Class Config
		//或是用 XmlWebApplicationContext 設定 XML Config 
		//來建立 Spring Application 上下文 (Context)
		AnnotationConfigWebApplicationContext springWebApplicationContext = new AnnotationConfigWebApplicationContext();
		springWebApplicationContext.register(SpringApplicationConfig.class); //這裡可以指定 Spring Class Config
//		XmlWebApplicationContext springWebApplicationContext = new XmlWebApplicationContext();
//		springWebApplicationContext.setConfigLocation("classpath:application-config.xml"); //這裡可以指定 Spring XML Config
		
		DispatcherServlet dispatcherServlet = new DispatcherServlet(springWebApplicationContext);
		
		ServletRegistration.Dynamic springDispatcherServlet = servletContext.addServlet("springDispatcherServlet", dispatcherServlet);
		springDispatcherServlet.setLoadOnStartup(1);
		springDispatcherServlet.addMapping("/");
		/** 上述 code 相當於 web.xml 中的以下 DispatcherServlet 設定
		 * <servlet>
				<servlet-name>springDispatcherServlet</servlet-name>
				<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
				<init-param>
					<param-name>contextConfigLocation</param-name>
					<param-value>/xx/xx/application-config.xml</param-value>
					</init-param>
				<load-on-startup>1</load-on-startup>
			</servlet>
			<servlet-mapping>
				<servlet-name>springDispatcherServlet</servlet-name> 
				<url-pattern>/</url-pattern>
			</servlet-mapping>
		 */
	}
}
src/main/com/config/SpringApplicatoinConfig.java (Spring Config 設定,取代 Spring XML Config File) :
package com.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@EnableWebMvc
@Configuration
@ComponentScan(basePackages = "com.*")
public class SpringApplicationConfig {
	
	@Bean
	public InternalResourceViewResolver setupViewResolver() {
		InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
		viewResolver.setPrefix("/WEB-INF/view/");
		viewResolver.setSuffix(".jsp");
		
		return viewResolver;
		
		/** 上述 code 相當於 Spring Config XML 的如下 InternalResourceViewResolver 設定
		 * <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> 
		        <property name="prefix">
		            <value>/WEB-INF/view/</value>
		        </property>
		        <property name="suffix">
		            <value>.jsp</value>
		        </property>
		    </bean>
		 */
	}
}


src/main/com/controller/TestController.java (測試用的 Controller) :
package com.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class TestController {
	
	@ResponseBody
	@RequestMapping("/test")
	public String test() {
		return "HIHI";
	}
	
	@RequestMapping("/hello")
	public String hello() {
		return "hello";
	}
	
}

src/main/webapp/WEB-INF/view/hello.jsp (測試用的 JSP View) : 
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello</title>
</head>
<body>
Say hello!
</body>
</html>

可以自己跑一個 Tomcat 起來試,應該可以成功存取以下 url

  1. http://localhost:8080/index.jsp (直接存取 webapp 下的 JSP Resource)
  2. http://localhost:8080/hello (會觸發 TestController.hello(),Request 會導向 webapp/WEB-INF/view/hello.jsp)
  3. http://localhost:8080/test (會觸發 TestController.test())

源碼下載分享:
javaee-spring-no-xml-test.7z


參考資料:

  1.  NO XML - Spring MVC Java-based configuration in 9 steps || Spring Annotation || Part 3

2024年8月25日 星期日

Webpack 使用 Bebel 並用 @babel/preset-typescript 使用 typescript

記錄下在使用 Webpack 時,
如何使用 Bebel 並用 @babel/preset-typescript 的方式配合使用 Typescript 的功能

將 @babel/preset-typescript 設定在 babel-loader 的 presets 裡就好了。

package.json:

webpack.config.js:

{
  "name": "xxx-proejct",
  "scripts": {
    "deploy": "webpack"
  },
  "devDependencies": {
    "@babel/core": "7.18.10",
    "@babel/preset-env": "7.18.10",
    "@babel/preset-typescript": "^7.24.7",
    "babel-loader": "8.2.5",
    "css-loader": "5.0.1",
    "mini-css-extract-plugin": "1.3.3",
    "sass": "1.54.9",
    "sass-loader": "10.1.0",
    "style-loader": "3.3.1",
    "webpack": "5.74.0",
    "webpack-cli": "4.10.0"
  }
}

webpack.config.js:

var path = require('path');
var MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
    entry: {
        index_js: {
            import: "./src/app.js",
            filename: "js/index_bundle.js"
        }
    },
    output: {
        path: __dirname + "/dist"
    },
    watch: false,
    devtool : 'source-map',
    mode : "production",
    resolve : {
     extensions : ['.js', '.ts'],
     modules: [path.resolve(__dirname, '.'), 'node_modules']
    },
    module: {
        rules : [{
            test : /\.(ts|js)$/,
            use : {
                loader : 'babel-loader',
                options : {
                    presets : ['@babel/preset-env', '@babel/preset-typescript']
                }
            }
        },
        {
          test: /\.s[ac]ss$/,
          use: [        
               MiniCssExtractPlugin.loader,
               "css-loader", "sass-loader"
              ]
        }]
    },
    plugins: [new MiniCssExtractPlugin({
        filename: "./css/index_bundle.css"
    })],
    externals: {
        angular: "angular"
    }
}

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

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(DBUnitExtension.class)
@DBUnit(escapePattern = "\"?\"",
	    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"};
	
	//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
	static void beforeAll() {
		//在所有 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,
    		 //雖然 Database Rider 有提供 executeStatementsBefore 和 executeStatementsAfter 參數,
             //但實測好像不太有效果,
             //因為例如我想在 DataSet 塞入 Database 後執行 PostgreSQL 的 setval() 重設遞增值 等 Sql 語句,
             //但 executeStatementsBefore 的 Sql 會在 Dataset 塞入 Database 之前執行,
             //而 executeStatementsAfter 好像都不會被執行,可能我理解有誤,
             //就算直接在 method 中 (例如 beforeEach() 裡) 執行 Sql 語句,它也會在 DataSet 塞入 Database 之前執行。
			 //所以最後我想如果有需要在 DataSet 塞入 Database 後執行的 Sql,
             //可能要放棄使用 @DataSet 的 Annotation (註解) 方式,
             //改成直接用程式碼直接執行 DataSet 塞入 Database 動作再執行我們要的 Sql 語句,
             //下方示範一個實測可以成功的 beforeEach() 和 afterAll()。
			 //SELECT setval('xxxTable_xxxField_seq', (SELECT coalesce(MAX(xxxField), 0) + 1 FROM xxxTable), false)
			 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)"})
	void beforeEach_donnoWhyNotWork() throws DatabaseUnitException, SQLException {
	}
    */
    
    @BeforeEach
	static void beforeEach() 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
	static void afterAll() 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));
		}
	}
	
	@Test
	void test() {
		//做你想做的 Test
	}
}

參考資料:

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