項目測試
項目測試是對項目的需求和功能進行測試,由測試人員寫出完整的測試用例,再按照測試用例執行測試。項目測試是項目質量的保證,項目測試質量直接決定了當前項目的交付質量。
測試人員在開展測試之前,首先需要進行測試的需求分析,測試需求分析包括:
測試內容:需要進行哪些方面的測試,包括功能測試、性能測試、可靠性測試、易用性測試和安全性測試等;
測試環境:測試環境的配置;
測試工具:選擇測試工具,包括缺陷管理工具和自動化測試工具等;
測試輪數:包括冒煙測試、功能測試、并發測試和弱網測試等。
測試需求分析完成后再開始項目測試。測試的步驟包括:冒煙測試、單元測試、集成測試、性能測試等,最后由產品經理完成產品的驗收。
單元測試
項目測試的第一步就是單元測試,單元測試也稱為模塊測試或組件測試。在項目開發過程中,單元測試用來檢查項目的單個單元或模塊是否正常工作,它是由開發人員在開發后立即在開發環境中完成的。
為什么要做單元測試
單元測試通常是軟件測試中基礎的測試類型,用于測試單獨模塊的功能是否有錯。它與功能測試的不同之處是,單元測試更加關注的是代碼內部的邏輯,而非功能的完整性。
根據上述描述,在項目中實施單元測試有以下5個目標:
隔離各部分代碼的功能;
確保單個模塊功能邏輯的正確性;
在開發的過程中及時發現代碼缺陷并修改;
發現產品開發早期邏輯中的Bug,以降低測試成本;
允許開發人員后續重構或升級代碼。
基于單元測試的目標,在項目中使用單元測試有以下4個優點:
能在產品開發周期的早期發現問題,可以大大地降低測試成本,因為早發現一個缺陷的成本要比晚期發現它的成本低得多。
在改變現有功能(回歸測試)的同時,可以減少缺陷。
簡化了調試過程(測試驅動開發就是基于測試用例來完成功能開發)。調試是在程序中發現并解決妨礙軟件正確運行缺陷的過程。當進行單元測試時,如果發現測試失敗,則只需要在調試代碼中做一下的更改,就可以快速定位錯誤。
進行單元測試,能夠保證代碼質量。
單元測試有哪些內容
單元測試的內容主要包括以下兩點:
單元測試的方法:通常使用白盒測試;
單元測試的類型:可以選擇手動測試或自動測試。
在對代碼進行單元測試時,可以使用手動的方式,也可以使用一些自動化工具。手動測試和自動化測試的區別主要體現在執行效率和操作等方面,如表7.1所示。
在實際開發中流行DevOps(Development+Operations)開發模式,因此建議讀者在項目中能使用自動化測試完成的任務盡量采用自動化測試來完成,以提升開發效率。
想要精通單元測試,還需要了解單元測試的幾個關鍵點:
(1)執行單元測試的時間:一般在開發完成后立即進行。
(2)誰做單元測試:開發人員進行自測。
(3)明確單元測試的具體任務,包括兩個方面:
首先,準備單元測試的計劃,包括:準備測試計劃;
回顧測試計劃;
修訂測試計劃;
定義單元測試計劃的基準數據。
其次,準備測試用例和腳本,包括:
準備測試環境、測試用例和腳本;
回顧測試用例和腳本;
修訂測試用例和腳本。
(4)定義單元測試用例和腳本的基準數據。
(5)執行單元測試,完成后出具單元測試報告。
常規的JUnit測試
JUnit是Java應用開發中使用最廣泛的單元測試框架。因為Java 8發布了Lambda表達式,使得Java的編碼風格發生了巨大的變化,所以JUnit團隊適時推出了新的框架——JUnit 5。JUnit 5能夠適應Lambda風格的編碼,建議在JDK 1.8及之后版本的項目中使用JUnit 5來創建和執行單元測試。本書中的單元測試以JUnit 5為例。
至于什么是JUnit,看以下官方的定義:
JUnit 5 is the next generation of JUnit. The goal is to create
an up-to-date foundation for developer-side testing on the JVM.
This includes focusing on Java 8 and above, as well as enabling
many different styles of testing.
官方提示JUnit 5是新一代的JUnit,它的目標是為JVM上的開發人員做測試創建一個最新的基礎,為Java 8及以上版本創建不同的測試風格。JUnit5框架=JUnit Platform+JUnit Jupiter+JUnit Vintage,其各部分框架的含義如下:
JUnit Platform是在JVM上啟動測試框架的基礎。
JUnit Jupiter是JUnit 5擴展的新的編程模型和擴展模型,用來編寫測試用例。Jupiter子項目為在平臺上運行Jupiter的測試提供了一個TestEngine(測試引擎)。
JUnit Vintage提供了一個在平臺上運行JUnit 3和JUnit 4的TestEngine。
JUnit 5的架構如圖7.1所示。
第一層測試用例:開發人員使用junit-jupiter-api等測試框架的API編寫業務代碼的單元測試。第二層測試引擎,JUnit測試框架實現引擎API的框架,jupiterengine和vintage- engine分別是JUnit 4和JUnit 5對測試引擎API的實現。
第三層junit-platform-engine:junit-platform-engine平臺引擎是對第二層中兩種不同引擎實現的抽象,是測試引擎的接口標準。
第四層IDEA:啟動器通過ServiceLoader發現測試引擎的實現并安排其執行。它為IDE和構建工具提供了API接口,因此在IDE中可以直接執行測試,例如,啟動測試并顯示其結果。
在項目中使用JUint 5進行單元測試時需要用到一些注解,如表7.2所示。此外,表中對JUnit 4和JUnit 5的注解使用進行了對比,供讀者參考。
根據以上的注解,下面使用JUint 5完成一個單元測試示例。
(1)新建一個項目,在pom.xml中添加JUnit 5的依賴,代碼如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.10.RELEASE</version>
<relativePath/> <!-- lookup parent from repository --></parent>
<groupId>com.example</groupId>
<artifactId>junit5-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>junit5-demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--加入JUnit 5的版本測試;如果想用JUnit 4進行測試,把exclusions去除-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
(2)完成一些簡單的業務代碼。新建一個用戶服務類的接口
UserService:package com.example.junit5demo.service;
import java.util.IllegalFormatException;
/**
* 測試接口
*/
public interface UserService {
/**
* 登錄
* @param userName
* @param password
* @return
* @throws IllegalFormatException
*/
boolean login(String userName,String password) throws
IllegalFormat Exception;
/**
* 查詢數量
* @return
*/
int countNum();
}
(3)新建一個類來實現UserService接口,這里使用的是最簡單的實現方式,暫時先不連接數據庫。
package com.example.junit5demo.service;
/**
* 測試接口的實現
*/
import org.springframework.stereotype.Service;@Service
public class UserServiceImpl implements UserService {
@Override
public boolean login(String userName, String password) throws
Illegal
ArgumentException {
if (userName == null || password == null
|| userName.isEmpty() || password.isEmpty()) {
throw new IllegalArgumentException("不能為空");
}
if ("cc".equals(userName) && "123".equals(password)) {
return true;
}
return false;
}
@Override
public int countNum() {
return 18;
}
}
(4)實現UserService接口的方法后,在類名上右擊,依次選擇Go To |Test | Create New Test...命令,具體的過程如圖7.2和圖7.3所示。按照上述操作會跳轉到選擇測試方法的頁面,如圖7.4所示。選擇要測試的方法,選中寫的兩個方法,單擊OK按鈕就能自動生成測試類和要測試的方法。
(5)添加Spring Boot的啟動類,代碼如下:
package com.example.junit5demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Junit5DemoApplication {
public static void main(String[] args) {
SpringApplication.run(Junit5DemoApplication.class, args);
}
}
(6)修改自動生成的測試類代碼,完成兩個業務方法的單元測試工作。
完成后的代碼如下:
package com.example.junit5demo.service;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@Slf4j
@SpringBootTest //SpringBot測試類注解
class UserServiceImplTest {
//輸入要測試的類
@Autowired
private UserService userService;
//在所有測試方法之前執行
@BeforeAll
public static void beforeAll() {
log.info("before all");
}
//在每個測試方法執行之前執行
@BeforeEach
public void beforeEach() {
log.info("before each");
}
@AfterEach
public void afterEach() {
log.info("after each");
}
@AfterAll
public static void afterAll() {
log.info("after all");
} //測試countNum方法
@Test
void countNum() {
int i = userService.countNum();
assertEquals(18, i);
assertNotEquals(1, i);
}
// 測試login方法
@Test
void login() {
boolean cc1 = userService.login("cc", "123");
assertEquals(cc1, true);
boolean cc2 = userService.login("cc2", "123");
assertEquals(cc2, false);
assertThrows(IllegalArgumentException.class, () ->
userService.login("", "123"));
assertThrows(IllegalArgumentException.class, () ->
userService.login("123", null));
}
}
(7)運行這個測試類,在IDEA的控制臺打印結果如圖7.5所示。可以看到左側的兩個方法都顯示綠色的勾,說明已經通過測試用例。至此,我們已經完成了UserService類中所有方法的單元測試,可以看出,相關代碼邏輯正確。
提示:在完成單元測試后,如果開發人員在測試過程中優化了代碼,對自己的代碼進行了重構,則需要對重構后的代碼再次進行單元測試,以確保其邏輯的正確性,千萬不能忽略這一點。
Mock測試
Mock測試是指在單元測試的過程中對一些不容易構造的對象模擬一個對象進行使用的過程。一些對象只能在特定的環境中產生,例如HttpServletRequest對象必須從Servlet容器中才能構造出來,ResultSet對象必須依賴JDBC的實現才能構造,ProceedingJoinPoint對象的構建必須依賴AOP的實現。在遇到這種復雜對象的構建時,使用一個虛擬的對象(Mock對象)來替代,使用一個“假”的對象,便于在測試時順利檢測復雜對象(虛擬對象)的使用邏輯,以便快速、準確地測試自己的代碼邏輯。
Mock的出現是為了解決不同的單元之間由于耦合而難于開發與測試的問題,因此在單元測試和集成測試中都會用到Mock。Mock最大的作用是把單元測試的耦合分解開,如果你的代碼對另一個類或者接口有依賴,則可以排除依賴,只驗證所調用依賴的行為。例如,需要測試UserService中的方法,但是它依賴UserDao,這時就直接模擬一個用戶的數據庫對象,且只測試UserService中的方法,來驗證USerService中的邏輯正確與否。
在進行單元測試時,以下的幾個場景需要用到Mock對象:
需要將當前被測單元和依賴模塊分離,構造一個獨立的測試環境,不關注被測單元的依賴對象,而只關注被測單元的功能邏輯。例如,被測代碼中需要依賴第三方接口的返回值進行邏輯處理,可能因為網絡或者其他環境因素,調用第三方平臺經常會中斷或者失敗,而無法對被測單元進行測試,這時就可以使用Mock技術來將被測單元和依賴模塊獨立開,使得測試可以進行下去。
被測單元依賴的模塊尚未開發完成,而被測單元需要依賴模塊的返回值進行后續處理。包括前后端分離項目中,后端接口開發完成之前,前端接口需要測試;依賴的上游項目的接口尚未開發完成,需要接口聯調測試;service層的代碼包含對Dao層的調用,但是Dao層的代碼還沒完成,需要模擬Dao層的對象。
被測單元依賴的對象較難模擬或者構造比較復雜。例如,HttpServletRequest對象和數據庫的連接對象Connection都非常難以構造,則可以直接使用模擬后的對象。
在Java項目開發中有很多Mock框架,常見的有Mockito和PowerMock。
Mockito是一個在項目中最常用的優秀的單元測試Mock框架,它能滿足大部分業務的測試要求;PowerMock框架可以解決Mockito框架不能解決的更難的問題,如業務代碼中的靜態方法、私有方法和Final方法等。PowerMock框架是在EasyMock和Mockito的基礎上進行擴展的,它通過提供定制的類進行加載器并進行一些字節碼的修改,從而實現更強大的測試功能。
本書使用Java開發中常用的Mockito作為Mock框架。下面介紹如何在Spring Boot項目中使用Mock對象進行測試,從而完成單元測試。
(1)在7.1.3小節中的項目文件pom.xml中添加Mockito依賴,代碼如下:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.11.2</version>
</dependency>
(2)新建一個產品服務類接口ProductService及其實現類,代碼如下:
package com.example.junit5demo.service;
public interface ProductService {
int countNum(); boolean productExists(String name);
}
(3)新建上述ProductService接口的實現類,代碼如下:
package com.example.junit5demo.service;
import com.example.junit5demo.dao.ProductDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductDao productDao;
@Override
public int countNum() {
return productDao.countNum();
}
@Override
public boolean productExists(String name) {
if (name == null || name.isBlank()) {
return false;
}
return productDao.productExists(name);
}
}
(4)新建一個ProductDao類,直接使用類的實現返回數據,代碼如下:
package com.example.junit5demo.dao;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class ProductDao {
public int countNum() {
return 2;
}
public boolean productExists(String name) {
/**
* 模擬Dao的方法
*/
List<String> apple = List.of("cc","apple", "orgage", "banana");
return apple.contains(name);
}
}
(5)生成ProductService的測試類,代碼如下:
package com.example.junit5demo.service;
import com.example.junit5demo.dao.ProductDao;
import org.junit.Assert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.mockito.Mockito.when;// 使用Spring的測試框架
@ExtendWith(SpringExtension.class)
@SpringBootTest
class ProductServiceImplTest {
/**
* 輸入要測試的對象
*/
@InjectMocks
ProductServiceImpl productService;
/**
* Mock對象的依賴對象
*/
@Mock
ProductDao productDao;
@BeforeEach
public void setUp() {
/**
* 初始化
*/
MockitoAnnotations.openMocks(this);
}
@Test
void coutNum() {
/**
* 當執行這個方法的時候直接返回5
*/
when(productDao.countNum()).thenReturn(5);
int num = productService.countNum();
/**
* 驗證返回值
*/
Assert.assertEquals(num,5);
}
@Test void productExists() {
/**
* 這里本來應該返回true,但是故意設置為false,再查看返回值
*/
when(productDao.productExists("cc")).thenReturn(false);
boolean cc = productService.productExists("cc");
Assert.assertEquals(cc,false);
}
@Test
void productExists3() {
when(productDao.productExists("apple")).thenReturn(false);
boolean cc = productService.productExists("apply");
Assert.assertEquals(cc,false);
}
}
(6)在本次測試用例中,需要測試ProductServiceImpl中的兩個業務方法。Product- ServiceImpl依賴ProductDao,這時模擬一個對象到測試對象中,當調用Dao的方法時就會返回設定的值,不會真正地執行Dao,而只測試ProductServiceImpl中的方法。執行本測試類ProductServiceImplTest中的所有測試方法,完成測試后的結果如圖7.6所示。可以看到,3個測試用例都已通過,說明已經完成了Mock單元測試。
以上就是使用Mock進行單元測試的過程。Mock測試是單元測試的一大利器,它能幫助開發人員更快地完成單元測試、業務代碼的檢測和Bug的修復。