單元測試的思考與實踐
1. 什么是單元測試
通常來說單元測試,是一種自動化測試,同時包含一下特性:
- 驗證很小的一段代碼(業務意義 或者 代碼邏輯 上不可再分割的單元),能夠更準確的定位到問題代碼的位置
- 能夠快速運行(單元測試的意義,在于快速且周期性的驗證原有代碼的準確性),提高項目開發效率
- 以隔離的方式 (isolated manner)運行(對外部依賴通過插樁解耦,避免單元測試的復雜度,實現問題快速定位,簡化單元測試的運行環境,多個單元測試可以以任何順序甚至并行進行)
2. 為什么要單元測試
因為單元測試有如下優點:
- 能快速的回歸,提高自測的效率
- 集成測試或者端到端的手工測試效率低,而且無法覆蓋到更細節的邏輯分支
- 也存在功能設計超前于產品設計,通過接口維度,無法觸達某些邏輯分支,需要通過單元測試來覆蓋
- 功能開發人員更了解代碼的實現,開發人員寫出的測試用例往往能更全面的覆蓋代碼
- 有良好單測的代碼,往往更方便重構
- 單元測試是項目代碼的一部分,維護方便,當然這也依賴良好的單元測試編寫習慣,合適的顆粒度
3. 如何識別有測試價值的代碼?
當我們考慮給代碼添加 單元測試時,需要首先考慮加入單測后能夠帶來的收益有多少,以及其付出的成本有多少,用最小的維護成本提供最高的價值的單元測試
3.1 項目屬性
軟件本身發布更新成本比較大,如嵌入式軟件,客戶端程序;或者 軟件的缺陷 更可能帶來較大的資損,如工廠,銀行內部的軟件,這類軟件都是需要優先考慮單元測試。
如果一個項目本身不是特別核心的項目,影響面小,迭代更新相對較容易,那么對單元測試的要求,或者說對質量的要求,也就沒有那么強烈
3.2 代碼屬性
3.2.1 重要的代碼
- 領域層
- 基礎設施代碼
3.2.2 不容易被集成測試覆蓋的代碼 - 邊界條件
- 異常條件
- 低概率場景
3.2.3 容易出現問題的代碼 - 復雜的業務邏輯分支
- 狀態機
- 膠水代碼:負責組合多個功能,多個功能的輸入具有不確定性
3.3 個人不建議的單元測試的行為
- 通常來說不建議在單元測試的時候,啟動spring容器后,會牽扯過多的外部依賴,導致單元測試難以進行,或者成本過高
- 同樣,外部接口,數據庫依賴,中間件依賴,都不建議在單元測試中加載,可以通過mock或者sub的方式來進行隔離
4. 編寫 Unit Test
通常按照單元測試的AAA模式來編寫單元測試,分為三部分:Arrange, Act, Assert
- Arrange
準備測試數據和測試環境,確保測試的可重復性和可預測性。這包括初始化對象、設置變量、模擬外部依賴等 - Act
執行實際的測試操作,也就是調用需要測試的方法或函數,并獲取返回值或狀態。這個階段應該僅包含單個操作,以確保測試的獨立性和可維護性 - Assert
驗證測試結果是否符合預期,也就是檢查實際的輸出是否與預期的輸出相同。如果結果不符合預期,我們需要檢查測試代碼和被測試代碼,找出問題所在并進行修復 - 結果驗證 - 對函數返回結果進行驗證
- 狀態驗證 - 對過程中的屬性值來進行驗證
- 行為驗證 - 對過程中會執行的動作進行驗證
spock測試框架代碼示例:
class OrderServiceImplTest extends Specification {
OrderService orderService = new OrderServiceImpl();
InventoryService inventoryService = Mock(InventoryService)
OrderConverter orderConverter = Mock(OrderConverter.class)
PaymentChannelClient paymentChannelClient = Mock(PaymentChannelClient)
OrderMapper orderMapper = Mock(OrderMapper)
def setupSpec() {} // runs once - before the first feature method
def setup() { // runs before every feature method
orderService.setInventoryService(inventoryService)
orderService.setPaymentChannelClient(paymentChannelClient)
orderService.setOrderMapper(orderMapper)
orderService.setOrderConverter(orderConverter)
}
def cleanup() {} // runs after every feature method
def cleanupSpec() {} // runs once - after the last feature method
def "create order correctly"() {
//準備測試需要的參數
given:
Long id = 1
CreateOrderCommand command = new CreateOrderCommand(orderNo, itemNo, orderItemQuantity, user, totalPrice)
//創建一個spy,可以用來做行為驗證
MockOrderEntity spyOrder = Spy(constructorArgs: [id, orderNo, itemNo, orderItemQuantity, null, user, totalPrice])
//指定返回spy
orderConverter.toEntity(_ as CreateOrderCommand) >> spyOrder
LockInventoryCommand lockInventoryCommand = new LockInventoryCommand(itemNo, orderItemQuantity)
when:
//觸發測試
Long resultId = orderService.createOrder(command)
then:
//行為驗證, 創建訂單的同時,執行鎖定庫存lockInventory會被執行一次,同時會驗證參數是否和我們提供lockInventoryCommand是否equals
1 * inventoryService.lockInventory(lockInventoryCommand)
//行為驗證,最終訂單執行insert
1 * spyOrder.insert()
//結果驗證,驗證返回的id
resultId == id
//狀態驗證
spyOrder.orderStatus == OrderStatus.CREATE
//以表格的形式提供測試數據集合
where:
orderNo | itemNo | orderItemQuantity | user | totalPrice
"1" | "it" | 10 | "userA" | 9.9
}
}
5. 如何自動化執行單元測試
使用spock框架進行單測,可以通過添加maven插件,來在maven打包的時候自動執行單元測試代碼
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>2.1-groovy-3.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.10.19</version>
<scope>test</scope>
</dependency>
<!-- Mandatory plugins for using Spock -->
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>1.12.0</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compileTests</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<configuration>
<includes>
<!-- 指定后綴為Test的文件,需要被執行單元測試 -->
<include>**/*Test</include>
</includes>
</configuration>
</plugin>
6. Spock測試框架中Mock,Stub,Spy的區別
- Stub(樁對象):Stub對象用于模擬被測試對象的某些行為。Stub對象通常用來模擬一些外部依賴(interface)返回指定數據,以便于進行單元測試。不能用于用來做行為驗證。
def ""() {
given:
def inventoryMapper = Stub(InventoryMapper)
InventoryServce inventoryService = new InventoryServiceImpl(inventoryMapper)
Inventory inv = new Inventory(10)
inventoryMapper.selectById(_) >> inv
when:
//inventoryService.stockOut(quantity, id)
inventoryService.stockOut(5, 1)
then:
inv.quantity == 5
}
- Mock(模擬對象):Mock對象和Stub對象類似,但是可以用來做行為驗證,所以在spock中通常可以用mock替代stub
def ""() {
given:
def inventoryMapper = Mock(InventoryMapper)
InventoryServce inventoryService = new InventoryServiceImpl(inventoryMapper)
Inventory inv = new Inventory(10)
inventoryMapper.selectById(_) >> inv
when:
//inventoryService.stockOut(quantity, id)
inventoryService.stockOut(5, 1)
then:
//行為驗證,inventoryMapper執行了一次stockOut
1 * inventoryMapper.stockOut(_)
inv.quantity == 5
}
3. Spy(監視對象):上面的Stub,Mock都是創建一個假的實例,而Spy是在真實實例的基礎上,類似創建一個包裝類,它可以記錄被測試對象的行為。既保留了原有實例功能的同時,還可以做行為驗證
```groovy
def ""() {
given:
def inventoryMapper = Stub(InventoryMapper)
InventoryServce inventoryService = new InventoryServiceImpl(inventoryMapper)
Inventory inv = Spy(Inventory)
inv.setQuantity(10)
inventoryMapper.selectById(_) >> inv
when:
//inventoryService.stockOut(quantity, id)
inventoryService.stockOut(5, 1)
then:
//行為驗證,inv執行了一次stockOut
1 * inv.stockOut(_)
inv.quantity == 5
}
通常來說調用Spy對象的方法,會被默認委托給真實的對象來執行,即執行真實的方法,但是Spy同樣也適用Stub行為,如:
def subscriber = Spy(SubscriberImpl, constructorArgs: ["Fred"])
//Spy對象也可以像 Stub對象一樣,替換掉receive方法,返回指定的值
subscriber.receive(_) >> "ok"
7. Partial Mocks(部分Mock)
7.1 callRealMethod
通常來說Mock可以對class或者interface創建一個fake對象,不會執行真實的方法,當在寫單元測試時有時會需要執行Mock對象的某些真實方法的時候,可以callRealMethod的方式來執行。
given:
def subscriber = Mock(SubscriberImpl)
//mock call方法
subscriber.call(_) >> {return "called"}
//通過callRealMethod指定mock對象執行原來的真實方法
subscriber.receive(_) >> { callRealMethod() }
then:
subscriber.receive("")
7.2 spy
通過callRealMethod是一種方式,另一種,就是通過Spy來實現,因為Spy是基于真實的對象創建的,那么就可以反過來實現一個對象既可以調用真實方法,又可以調用假的方法
given:
def subscriber = Spy(SubscriberImpl, constructorArgs: ["Fred"])
//mock call方法
subscriber.call(_) >> {return "called"}
then:
//這里會直接執行真實方法
subscriber.receive("")
posted on 2024-04-03 16:32 mindSucker 閱讀(70) 評論(0) 收藏 舉報
浙公網安備 33010602011771號