Java如何優(yōu)雅地實現(xiàn)單元測試與集成測試
本文轉載自微信公眾號「 無敵碼農(nóng)」,作者 無敵碼農(nóng)。轉載本文請聯(lián)系 無敵碼農(nóng)公眾號。
在日常的開發(fā)過程中,為了保證代碼質量,有追求的程序員一般都會對自己編寫的代碼進行充分的測試,這種測試不僅僅是體現(xiàn)在對正常功能的簡單接口調用,而是要根據(jù)代碼中的各種邏輯分支,進行盡可能多的覆蓋性單元測試以及主要邏輯的集成測試。
上面說到的測試對于程序員來說,絕不僅僅只是依賴于Postman之類的網(wǎng)絡工具,而要以編寫獨立的單元/集成測試代碼的方式來實現(xiàn),具體來說在Java中就是要基于JUnit、Mocktio之類的測試框架編寫相應的UT及IT代碼,并在這個過程中提前發(fā)現(xiàn)軟件Bug、重新審視所寫代碼并進行優(yōu)化。
實話說編寫測試代碼對提高軟件質量,及自身編程水平來說都是一種非常有用的手段。但在工作中,并不是所有人都能正確地掌握單元測試和集成測試代碼的寫法和組織形式。以Maven工程代碼為例,很多人會把單元測試和集成測試代碼弄混,這樣導致的后果就是大部分Maven工程代碼:"mvn test"幾乎很難跑通。
而本文想要表達的內容就是如何在Maven工程中有效的區(qū)分和組織單元測試、集成測試代碼使得它們互不干擾,并具體演示它們的寫法。
Maven測試代碼結構的組織
我們知道在Maven工程結構中“src/test”目錄是專門用于存放測試代碼的,但令人痛苦的是Maven的標準目錄結構只定義了這樣一個測試目錄,也就是說它本身是無法單獨區(qū)分單元測試代碼和集成測試代碼的,這也是為什么很多人會把UT和IT代碼同時寫到"src/test"目錄而導致“mvn test”難以跑過的原因。
那么有什么辦法可以友好地解決這個問題呢?在接下來的內容中我們以Maven構建Spring Boot項目為例來具體演示下在Maven中如何友好地分離UT及IT,具體步驟如下:
1)、首先我們創(chuàng)建一個基于Maven構建的Spring Boot項目,代碼結構如下圖所示:
如上圖所示,在規(guī)劃的目錄結構中我們將IT的代碼目錄及資源文件目錄單獨分離在“src/integration-test”目錄下,默認的“src/test”目錄還是作為存放UT代碼的目錄,而Maven在構建的過程中默認只運行UT代碼。這樣即便IT代碼由于網(wǎng)絡、環(huán)境等原因無法正常執(zhí)行,但也不至于影響到UT代碼的運行。
2)、創(chuàng)建區(qū)分UT、IT代碼的Maven Profiles文件
默認情況下Maven是無法主動識別“src/test”目錄之外的測試代碼的,所以當我們將IT代碼抽象到"src/integration-test"目錄之后,需要通過編寫Maven Profiles文件來進行區(qū)分,具體示意圖如下:
如上圖所示,我們可以在與“src”目錄平行創(chuàng)建一個“profiles”的目錄,其中分別用“dev”、“integration-test”目錄中的config.properties文件來進行區(qū)分,其中dev目錄下的config.properties文件的內容為:
- profile=dev
而integration-test目錄中的config.properties文件則為:
- profile=integration-test
3)、通過pom.xml文件配置上述profiles文件生效規(guī)則
為了使得這些profiles文件生效,我們還需要在pom.xml文件中進行相應的配置。具體如下:
- <!--定義關于區(qū)分集成測試及單元測試代碼的profiles-->
- <profiles>
- <!-- The Configuration of the development profile -->
- <profile>
- <id>dev</id>
- <activation>
- <activeByDefault>true</activeByDefault>
- </activation>
- <properties>
- <build.profile.id>dev</build.profile.id>
- <!--Only unit tests are run when the development profile is active-->
- <skip.integration.tests>true</skip.integration.tests>
- <skip.unit.tests>false</skip.unit.tests>
- </properties>
- </profile>
- <!-- The Configuration of the integration-test profile -->
- <profile>
- <id>integration-test</id>
- <properties>
- <build.profile.id>integration-test</build.profile.id>
- <!--Only integration tests are run when the integration-test profile is active-->
- <skip.integration.tests>false</skip.integration.tests>
- <skip.unit.tests>true</skip.unit.tests>
- </properties>
- </profile>
- </profiles>
上述內容先定義了區(qū)分dev及integration-test環(huán)境的的profile信息,接下來在build標簽中定義資源信息及相關plugin,具體如下:
- <build>
- <finalName>${project.artifactId}</finalName>
- <!--步驟1:單元測試代碼、集成測試代碼分離-->
- <filters>
- <filter>profiles/${build.profile.id}/config.properties</filter>
- </filters>
- <resources>
- <resource>
- <filtering>false</filtering>
- <directory>src/main/java</directory>
- <includes>
- <include>**/*.properties</include>
- <include>**/*.xml</include>
- <include>**/*.tld</include>
- <include>**/*.yml</include>
- </includes>
- </resource>
- <!--步驟2:通過Profile區(qū)分Maven集成測試代碼、單元測試代碼目錄-->
- <resource>
- <filtering>true</filtering>
- <directory>src/main/resources</directory>
- <includes>
- <include>**/*.properties</include>
- <include>**/*.xml</include>
- <include>**/*.tld</include>
- <include>**/*.yml</include>
- <include>**/*.sh</include>
- </includes>
- </resource>
- </resources>
- <plugins>
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- </plugin>
- <!-- 步驟三:將源目錄和資源目錄添加到構建中 -->
- <plugin>
- <groupId>org.codehaus.mojo</groupId>
- <artifactId>build-helper-maven-plugin</artifactId>
- <version>3.1.0</version>
- <executions>
- <!-- Add a new source directory to our build -->
- <execution>
- <id>add-integration-test-sources</id>
- <phase>generate-test-sources</phase>
- <goals>
- <goal>add-test-source</goal>
- </goals>
- <configuration>
- <!-- Configures the source directory of our integration tests -->
- <sources>
- <source>src/integration-test/java</source>
- </sources>
- </configuration>
- </execution>
- <!-- Add a new resource directory to our build -->
- <execution>
- <id>add-integration-test-resources</id>
- <phase>generate-test-resources</phase>
- <goals>
- <goal>add-test-resource</goal>
- </goals>
- <configuration>
- <!-- Configures the resource directory of our integration tests -->
- <resources>
- <resource>
- <filtering>true</filtering>
- <directory>src/integration-test/resources</directory>
- <includes>
- <include>**/*.properties</include>
- </includes>
- </resource>
- </resources>
- </configuration>
- </execution>
- </executions>
- </plugin>
- <!--步驟四:Runs unit tests -->
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-surefire-plugin</artifactId>
- <version>2.18</version>
- <configuration>
- <!-- Skips unit tests if the value of skip.unit.tests property is true -->
- <skipTests>${skip.unit.tests}</skipTests>
- <!-- Excludes integration tests when unit tests are run -->
- <excludes>
- <exclude>**/IT*.java</exclude>
- </excludes>
- </configuration>
- </plugin>
- <!--步驟五:Runs integration tests -->
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-failsafe-plugin</artifactId>
- <version>2.18</version>
- <executions>
- <execution>
- <id>integration-tests</id>
- <goals>
- <goal>integration-test</goal>
- <goal>verify</goal>
- </goals>
- <configuration>
- <skipTests>${skip.integration.tests}</skipTests>
- </configuration>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
到這里我們就完成了基于Maven構建的Spring Boot項目的UT及IT代碼目錄的分離配置,此時對UT代碼的執(zhí)行還是通過默認“mvn test”命令,而集成測試代碼的運行則可以通過如下命令:
- mvn clean verify -P integration-test
單元測試代碼示例
通過前面的配置操作就完成了單元測試、集成測試代碼目錄的分離設置。在后續(xù)的開發(fā)過程中只需要將相應的測試代碼寫在對應的測試目錄即可。接下來我們模擬一段業(yè)務邏輯并演示如何編寫其對應的UT代碼。具體如下:
如上圖所示,參考MVC三層規(guī)范,我們編寫了一個接口邏輯,該接口Controller層接收Http請求后調用Service層進行處理,而Service層處理邏輯時會調用Dao層操作數(shù)據(jù)庫,并將具體信息插入數(shù)據(jù)庫。
那么我們編寫單元測試(UT)代碼時,針對的是單獨的某個邏輯單元的測試,而不是從頭到位的整個邏輯,它的運行不應該依賴于任何網(wǎng)絡環(huán)境或其他組件,所有依賴的組件或網(wǎng)絡都應該先進行Mock。以單元測試TestServceImpl中的“saveTest”方法為例,其UT代碼編寫如下:
- @RunWith(SpringRunner.class)
- @SpringBootTest(classes = TestServiceImpl.class)
- @ActiveProfiles("test")
- public class TestServiceImplTest {
- @Autowired
- TestServiceImpl testServiceImpl;
- @MockBean
- TestDao testDao;
- @Test
- public void saveTest() {
- //調用測試方法
- testServiceImpl.saveTest("無敵碼農(nóng)微信公眾號");
- //驗證執(zhí)行測試的邏輯中是否調用過addUser方法
- verify(testDao).addUser(any());
- }
- }
如上所示UT代碼,我們UT測試的主要對象為TestServiceImpl類,所以可以在@SpringBootTest注解中進行范圍指定。而@ActiveProfiles("test")則表示代碼中所依賴的系統(tǒng)參數(shù),可以從測試資源目錄resouces/application-test.yml文件中獲得。
單元測試的主要目的是驗證單元代碼內的邏輯,對于所依賴的數(shù)據(jù)庫Dao組件并不是測試的范圍,但是沒有該Dao組件對象,UT代碼在執(zhí)行的過程中也會報錯,所以一般會通過@MockBean注解進行組件Mock,以此解決UT測試過程中的代碼依賴問題。此時運行“mvn test”命令:
單元測試代碼得以正常執(zhí)行!
集成測試代碼示例
在Spring Boot中UT代碼的編寫方式與IT代碼類似,但是其執(zhí)行范圍是包括了整個上下文環(huán)境。我們以模擬從Controller層發(fā)起Http接口請求為例,來完整的測試整個接口的邏輯,并最終將數(shù)據(jù)存入數(shù)據(jù)庫。具體測試代碼如下:
- @RunWith(SpringRunner.class)
- @SpringBootTest
- @ActiveProfiles("test")
- public class ITTestControllerTest {
- @Autowired
- TestController testController;
- @Test
- public void saveTest() {
- testController.saveTest("無敵碼農(nóng)微信公眾號");
- }
- }
可以看到對于集成測試代碼在@SpringBootTest中并沒有指定具體的類,它的默認執(zhí)行范圍為整個應用的上下文環(huán)境。而代碼中的依賴組件由于整個應用上下文都會被啟動,所以依賴上并不會報錯,可以理解為是一個正常啟動的Spring Boot應用。
需要注意的是由于IT代碼的目錄有獨立的資源配置,所以相關的依賴配置,如數(shù)據(jù)庫等需要在“src/integration-test/resouces/application-test.yml”文件中單獨配置,例如:
- spring:
- application:
- name: springboot-test-demo
- #數(shù)據(jù)庫邏輯
- datasource:
- url: jdbc:mysql://127.0.0.1:3306/test
- username: root
- password: 123456
- type: com.alibaba.druid.pool.DruidDataSource
- driver-class-name: com.mysql.jdbc.Driver
- separator: //
- server:
- port: 8080
此時運行集成測試命令“mvn clean verify -P integration-test”:

可以看到執(zhí)行IT測試代碼得以正常執(zhí)行!
后記
本文著重介紹了在Java項目中如何編寫單元測試(UT)和集成測試(IT)代碼的工程實踐。在日常編寫代碼的過程中,良好的測試代碼編寫是一種非常好的習慣,一般來說對于UT或IT代碼執(zhí)行錯誤的工程,要求嚴格的團隊會讓其構建的過程中無法通過,以此來嚴格要求團隊成員。
原文鏈接:https://mp.weixin.qq.com/s/RT-KKT1BskUYEvYAXhms5A