談起測試
其實測試這個東西在我們項目流程中是必需的一個步驟。
那到底測試是什麼?他到底承擔了一個怎樣的職責?
完整的軟件測試工作包括單元測試、集成測試、確認測試和系統測試工作。單元測試工作主要在編碼階段完成,由開發人員和軟件測試工程師共同完成,其主要依據是詳細測試。
確認測試和系統測試是在軟件開發完成後,驗證軟件的功能與需求的一致性、驗證軟件在相應的硬件條件下的系統功能是否滿足用戶需求,其主要依據是用戶需求。
測試從下到上大致可以分爲單元測試
、端到端測試
和系統集成測試
。
單元測試是最基礎的,一般其代碼量也是最多的,一般是針對函數、方法和類的測試,但其寫好後改動一般是最小的,單元測試是其他測試的基石。
端到端測試是基於單元測試之上的,主要針對API和接口的測試,由於只針對接口進行測試,相對單元測試,端到端測試代碼量更少,但面對需求的變更其測試代碼也更容易變更。
系統集成測試主要是對整個系統進行測試,針對地是客戶端的使用界面。
以現在主流的前後端分離來說明:
單元測試 | 端到端測試 | 系統集成測試 |
---|---|---|
針對視圖層中的某個視圖方法的測試,或者針對模型層中某個orm的測試 | 模擬接口請求進行測試 | 模擬用戶操作進行測試 |
往往開發人員不喜歡測試,主要可能有以下幾個原因:
-
反正有測試人員會測試
-
時間緊迫
-
太麻煩了
綜合各種外部以及內部因素,造成了以下場景:
在各種跟測試人員溝通中,項目以各種形式的加班後上線,各方在分析項目問題的時候,就抓着項目的質量各種討伐,到底是誰的鍋?
作爲一個開發人員,我只能說:其實開發人員真的很累,bug也是改不完的,但是這不是造成項目質量問題的藉口。
誠然讓開發人員完全是承擔測試的保障是不科學的,BUT 你寫的代碼你需要去負責它的功能,保證它符合需求,不管有沒有測試或者其他人在你背後。 這個我覺得是開發人員的自覺性以及本身職責所在。
雖然我上面講得義正言辭似的,但我也曾是一個不喜歡測試的開發者,我現在也不是一個代碼測試覆蓋率很高的開發者,我只是一個努力的工具人,努力提高自己代碼質量的開發者。
在我學習的過程中,一些前輩跟我說,“不要覺得編寫測試代碼麻煩浪費時間,在合適的時間點提高項目代碼的覆蓋率,利用我們最擅長的能力去保障我們的功能正確性以及穩定性。” 接下來我將從如何利用代碼去完成我們能做的測試——單元測試以及部分性能測試,提供我們常用的測試代碼示例,主要以Java語言爲主,希望大家在投入項目開發過程的時候可以以此作爲參考,同時達到有效減少測試向我們扔BUG的行爲。
開發如何測試
開發自測一般兩種方式就是基於UI進行功能測試
以及代碼腳本輔助測試
。結合我們常見的MVC模型來說,通過頁面進行UI測試,是屬於View層的測試,我們常說的單元測試則是針對View層以下的層Controller以及Model的子模塊方法,開發人員根據開發的功能模塊以及開發階段,選擇不同的方式驗證功能的正確性以及穩定性
。
我們大部分開發者無法寫得跟測試人員一樣優秀的測試用例,但是我們可以使用我們最擅長的工具語言去表達我們驗證代碼的邏輯。
以下內容將主要基於Java語言,分別從單元測試以及性能測試上面詮釋開發如何測試這個題目。
單元測試
認識單元測試
單元測試是用來對一個模塊、一個函數或者一個類來進行正確性檢驗的測試工作。
單元在質量保證中是非常重要的環節,根據測試金字塔原理,越往上層的測試,所需的測試投入比例越大,效果也越差,而單元測試的成本要小的多,也更容易發現問題。
單元測試的意義
來自從頭到腳說單測——談有效的單元測試說到的一個經典的單元測試意義的解釋
因此總結起來,單元測試的意義就是:
- 單元測試對我們的產品質量是非常重要的。
- 單元測試是所有測試中最底層的一類測試,是第一個環節,也是最重要的一個環節,是唯一一次有保證能夠代碼覆蓋率達到100%的測試,是整個軟件測試過程的基礎和前提,單元測試防止了開發的後期因bug過多而失控,單元測試的性價比是最好的。
- 據統計,大約有80%的錯誤是在軟件設計階段引入的,並且修正一個軟件錯誤所需的費用將隨着軟件生命期的進展而上升。錯誤發現的越晚,修復它的費用就越高,而且呈指數增長的趨勢。作爲編碼人員,也是單元測試的主要執行者,是唯一能夠做到生產出無缺陷程序這一點的人,其他任何人都無法做到這一點。
- 代碼規範、優化,可測試性的代碼
- 放心重構的資本
單元測試的實施
在《單元測試的藝術》這本書提到一個案例:找了開發能力相近的兩個團隊,同時開發相近的需求。進行單測的團隊在編碼階段時長增長了一倍,從7天到14天,但是,這個團隊在集成測試階段的表現非常順暢,bug量小,定位bug迅速等。最終的效果,整體交付時間和缺陷數,均是單測團隊最少。
單測,存在即合理。一方面,需要把單測放在整個迭代週期來觀測其效果;一方面,寫單測也是技術活,寫得好的同學,時間少代碼質量高(也即,不是說寫了單測,就能寫好單測)
單元測試的階段
廣義的單元測試,我們指這三部分的有機組合:
因此遵循這個過程,一開始從單元測試用例編寫開始,然後一個階段後進行靜態代碼掃描,主要是使用sonarqube
,接着針對掃描結果進行code review,分析問題,不斷優化代碼,提高代碼質量。
細化單元測試的編寫過程就是:
-
數據準備
在編寫測試用例前,需要依賴到一些數據,根據入參準備測試數據。 -
構造參數及打樁(stub)
調用方法需要傳遞入參。
打樁科普:
mock是一種用於單元測試數據模擬的技術,俗稱打樁技術。
mock的好處:
1、並行工作:藉助mock,接口之間可以實現解耦合,實現測試驅動開發
2、隔離系統:可以模擬請求,避免數據庫的污染,同時可以做界面演示 -
執行測試
這一步比較簡單,直接調用被測方法即可。 -
結果驗證
這裏除了驗證被測方法的返回值外,還需要驗證插入到數據庫中的數據是否正確,某外部方法被調用過n次或未調用過。 -
必要的清理
對打樁進行清理,對數據庫髒數據進行清理。
編寫單元測試
一個基本的單元測試編寫
示例代碼
我們先通過一個示例來看如何編寫測試。假定我們編寫了一個計算疊加的類,它只有一個靜態方法來計算疊加:
S=1+2+…+n
示例代碼如下:
public class TestDemo {
public static long superimposed(long n) {
long r = 0;
for (long i = 1; i <= n; i++) {
r += i;
}
return r;
}
}
生成測試用例
基於Junit4框架編寫我們的測試用例
JUnit 是 Java 編程語言的單元測試框架,用於編寫和可重複運行的自動化測試。
使用JUnit編寫單元測試的好處在於,我們可以非常簡單地組織測試代碼,並隨時運行它們,JUnit就會給出成功的測試和失敗的測試,還可以生成測試報告,不僅包含測試的成功率,還可以統計測試的代碼覆蓋率,即被測試的代碼本身有多少經過了測試。
使用Idea自帶的生成測試用例功能,勾選我們需要測試的方法以及測試框架
測試代碼如下:
public class TestDemoTest {
@Test
public void superimposed() {
assertEquals(1, TestDemo.superimposed(1));
assertEquals(3, TestDemo.superimposed(2));
assertNotEquals(50, TestDemo.superimposed(10));
}
}
上面有列舉了3個測試條件分別是:
- 當入參等於1的時候,輸出結果應該等於1
- 當入參等於2的時候,輸出結果應該等於3
- 當入參等於10的時候,輸出結果應該不等於50
執行測試
Run/Debug執行測試,測試結果Pass即通過
常用的斷言方法介紹:
- assertEquals() 如果比較的兩個對象是相等的,此方法將正常返回;否則失敗顯示在 JUnit 的窗口測試將中止。
- assertSame() 和 assertNotSame() 方法測試兩個對象引用指向完全相同的對象。
- assertNull() 和 assertNotNull() 方法測試一個變量是否爲空或不爲空(null)。
- assertTrue() 和 assertFalse() 方法測試 if 條件或變量是 true 還是 false。
- assertArrayEquals() 將比較兩個數組,如果它們相等,則該方法將繼續進行不會發出錯誤。否則失敗將顯示在 JUnit 窗口和中止測試。
SpringBoot項目測試用例編寫
示例工程
工程代碼結構如下,這是一個基於SpringBoot實現對user對象進行CRUD的基礎工程
│ DemoApplication.java
|
└─user
├─controller
│ UserController.java
│
├─dao
│ UserDao.java
│
├─entity
│ User.java
│
└─service
│ UserService.java
│
└─impl
UserServiceImpl.java
User對象主要包含以下兩個屬性
@Getter
@Setter
public class User implements Serializable {
private static final long serialVersionUID = -99146895849578786L;
private int id;
private String name;
假設我們要對UserService的新增User邏輯進行測試.
新增User邏輯:新增User的時候,假如Name屬性值爲空的時候,不插入數據到數據庫直接返回Null,否則新增成功,並返回user對象。
@Override
public User insert(User user) {
if(StringUtils.isBlank(user.getName())){
return null;
}
this.userDao.insert(user);
return user;
}
基於spring-boot-starter-test編寫測試用例
由於是基於SpringBoot的工程,我們可以直接使用spring-boot-starter-test
框架編寫測試用例
springboot測試步驟
直接在測試類上面加上如下2個註解
@RunWith(SpringRunner.class)
@SpringBootTest
就能取到spring中的容器的實例,如果配置了@Autowired那麼就自動將對象注入。
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserServiceImplTest {
@Autowired
UserService userService;
@Test
public void insert() {
String name = "test2020062101";
User posUser = new User();
posUser.setName(name);
User res = userService.insert(posUser);
Assert.assertNotNull("驗證返回結果不爲空", res);
posUser.setName(null);
res = userService.insert(posUser);
Assert.assertNull("驗證返回結果爲空", res);
}
執行測試
Run/Debug測試類,查看測試結果
查看數據新增記錄
測試打樁
有個很常見的情形,在開發中有可能你調用的其他服務沒有開發完,比如實現類邏輯還沒有確定,我們可以通過規定其入參以及對應的返回值來模擬這個bean的邏輯,或者根據某個情形下進行某個路由操作的選擇(如果入參是A則結果爲B,如果爲C則D)。這種模擬也被成爲測試打樁。
以上面的SpringBoot工程爲例,我們假定UserDao裏面的queryByName邏輯沒有實現,我們需要去驗證UserService的使用name查詢User對象邏輯.
UserService.queryByName
@Override
public User queryByName(String name) {
if(StringUtils.isBlank(name)){
return null;
}
return this.userDao.queryByName(name);
}
測試用例
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserServiceImplTest {
@Autowired
UserService userService;
@MockBean
private UserDao userDao;
@Test
public void queryByName() throws Exception {
String name = "test2020062101";
User posUser = new User();
posUser.setId(100);
posUser.setName(name);
// 定義當調用mock userDao的queryByName()方法,並且參數爲test2020062101時,
// 就返回id爲100、name爲test2020062101的user對象
Mockito.when(userDao.queryByName(name)).thenReturn(posUser);
// 返回的會是名字爲test2020062101的user對象
User user = userService.queryByName(name);
Assert.assertNotNull(user);
Assert.assertEquals(user.getId(), 100);
Assert.assertEquals(user.getName(), name);
}
}
這裏關注userDao的引入,是使用@MockBean
,當 userDao 被加上這個註解之後,表示 Mockito
會幫我們創建一個假的 mock 對象,替換掉 Spring 中已存在的那個真實的 userDao bean,也就是說,注入進 userService 的 userDao bean,已經被我們替換成假的 mock 對象了,所以當我們再次調用 userService 的方法時,會去調用的實際上是 mock userDao bean 的方法, 而不是真實的 userDao bean。
當我們創建了一個假的 userDao 後,我們需要爲這個 mock userDao 自定義方法的返回值,這裏有一個公式用法,下面這段代碼的意思爲,當調用了某個 mock 對象的方法時,就回傳我們想要的自定義結果
Mockito.when( 對象.方法名() ).thenReturn( 自定義結果 )
Mockio基本使用
1.thenReturn 系列方法
thenReurn
:主要根據假定條件返回預設的結果
當調用mock userDao的queryByName()方法,並且參數爲test2020062101時就返回id爲100、name爲test2020062101的user對象
Mockito.when(userDao.queryByName("test2020062101"))
.thenReturn(new User(100,"test2020062101"));
當調用mock userDao的queryByName()方法,並且任意String類型參數都返回id爲100、name爲test2020062101的user對象
Mockito.when(userDao.queryByName(Mockito.anyString()))
.thenReturn(new User(100,"test2020062101"));
2.thenThrow 系列方法
thenThrow
:主要根據假定條件拋出預設的異常
當調用mock userDao的queryByName()方法,並且參數爲886時就拋RuntimeException異常
Mockito.when(userService.queryByName("886"))
.thenThrow(new RuntimeException("mock throw exception"));
user = userService.queryByName("886"); //會拋出一個RuntimeException
當沒有參數的時候,即是方法定義爲public void myMethod() {...})
,要改用````doThrow() 拋出 Exception
假如有個public void print(){}
Mockito.doThrow(new RuntimeException("mock throw exception")).when(userService).print();
3.verify 系列方法
verify
: 用於驗證調用次數,我們可以在測試方法代碼的末尾使用Mockito驗證方法,以確保調用了指定的方法。
檢查調用 userService 的queryByName、且參數爲"886"的次數是否爲1次
Mockito.verify(userService, Mockito.times(1)).queryByName(Mockito.eq(""886"")) ;
上述就是 Mockito 的 mock 對象使用方法,不過當使用 Mockito 在 mock 對象時,有一些限制需要遵守
- 不能 mock 靜態方法
- 不能 mock private 方法
- 不能 mock final class
因此在寫代碼時,需要做良好的功能拆分,才能夠使用 Mockito 的 mock 技術,幫助我們降低測試時 bean 的耦合度。
結語
還有一個章節是性能測試——如何使用框架測試代碼性能
,由於篇幅問題將放在下一章。
就單元測試這塊,其實基於斷言以及apring-boot-test框架以及爲我們去編寫單元測試用例提供了極大的便利,因爲不要再說“太麻煩了”。上面着重介紹了一下Mockio,其實大家可以嘗試在寫代碼時,從 mock 測試的角度來寫,更能夠寫出功能切分良好的代碼架構。
還有一點要提的是,所有的測試工具框架都是輔助效率作用,核心還是在開發者的開發邏輯以及驗證功能邏輯上面,這方面需要我們不斷積累開發經驗,汲取相關知識,希望大家一起共同努力,成爲一個更優秀的開發者。
參考資料
JUnit4教程&實踐
從頭到腳說單測——談有效的單元測試
有贊單元測試實踐
SpringBoot 單元測試利器——Mockito