1 概述
測試的種類很多:單元測試、集成測試、系統測試等,程序員寫代碼進行測試的可以稱為白盒測試,單元測試和集成測試都可以進行白盒測試,可以理解為單元測試是對某個類的某個方法進行測試,集成測試則是測試一連串的方法。測試代碼也是代碼,即使要求測試代碼邏輯簡單,但寫單元測試代碼也是要花時間的,維護也是要花時間的,所以在寫測試代碼上也要有些平衡。單元測試的顆粒度比較小,最接近方法的業務邏輯,應該詳盡的測試;而集成測試牽扯的方法比較多,就比較復雜,就挑重點的地方做測試。本文先了解單元測試。
2 單元測試
2.1 業務邏輯測試
在DDD里有一種思想,就是要把業務邏輯分離出來,這部分業務邏輯是業務的核心,應該是公司的核心價值所在,所以應該進行詳盡測試。測試要做得多,成本就要低,所以這塊的測試主要集中在邏輯方面,業務邏輯的代碼也要寫成函數的形式,也就是給予一定的參數,就會反饋相同的結果。這塊代碼不應該跟數據庫、中間件等扯上關系,否則就很難做到輕量化。
有不少業務邏輯代碼是比較簡單的,不一定會按DDD的模式進行文件或者目錄分離,但也要在類或者方法層面做到把業務邏輯分離,以便對這些業務進行良好的單元測試。這種測試僅需要對一些類進行mock,然后就跟測試一個有參數有返回值的方法那樣簡單。這就要求涉及到數據庫或者中間件等外部資源的操作,都應該接口化,這樣就比較容易進行mock。
2.2 測試例子
假設有個創建組成員的功能,為這個業務邏輯創建一個服務接口GroupMemberCreator和服務類GroupMemberCreatorImpl,里面有個方法create(GroupMember member),用于編寫創建成員的業務邏輯,有部分業務會封裝到業務對象GroupMember中;這個方法里需要把數據存儲到數據庫,則通過GroupMemberRepository提供save()接口把數據存儲到數據庫中,該接口只有數據庫相關操作,而沒有業務邏輯。
// 把業務邏輯放到GroupMember和GroupMemberCreatorImpl里
public class GroupMember {private Long groupId;private String name;public GroupMember(Long groupId, String name) {// 做業務邏輯:校驗參數并組裝GroupMember信息this.groupId = groupShouldExist(groupId);this.name = memberNameShouldNotExist(name);}public Long getGroupId() {return groupId;}public String getName() {return name;}private Long groupShouldExist(Long groupId) {// 校驗組存在,否則拋異常return groupId;}private String memberNameShouldNotExist(String name) {// 校驗成員是否已經存在,否則拋異常return name;}
}public interface GroupMemberCreator {GroupMember create(Long groupId, String memberName);
}
@Service
public class GroupMemberCreatorImpl implements GroupMemberCreator {private GroupMemberRepository repository;@Autowiredpublic GroupMemberCreatorImpl(GroupMemberRepository repository) {this.repository = repository;}@Overridepublic GroupMember create(Long groupId, String memberName) {GroupMember member = new GroupMember(groupId, memberName);return repository.save(member);}
}// Repository只有數據庫相關操作,無業務邏輯
public interface GroupMemberRepository {GroupMember save(GroupMember member);
}
@Repository
public class GroupMemberReposistoryImpl implements GroupMemberRepository {@Overridepublic GroupMember save(GroupMember member) {// 調相關DAO操作數據庫return member;}
}// 測試用例例子
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static org.mockito.ArgumentMatchers.any;
public class GroupMemberCreatorImplTest {@Testpublic void create_new_member() {Long groupId = 1L;String name = "Joy";// 1. mock對象// 調repository.save()的時候,返回一個mock對象,不真正調用數據庫GroupMember mockMember = new GroupMember(groupId, name);GroupMemberRepository repository = Mockito.mock(GroupMemberRepository.class);Mockito.when(repository.save(any())).thenReturn(mockMember);// 2. 測試代碼邏輯GroupMemberCreatorImpl creator = new GroupMemberCreatorImpl(repository);GroupMember member = creator.create(groupId, name);// 3. 斷言(Assertion)Assertions.assertThat(member).isNotNull();Assertions.assertThat(member.getGroupId()).isEqualTo(groupId);Assertions.assertThat(member.getName()).isEqualTo(name);}
}
從上面測試用例來看,測試的三個步驟:一是用mockito進行mock對象,指定mock對象執行方法的返回值(可根據參數返回);二是調業務邏輯代碼進行測試;三是對測試結果進行斷言。
通過測試可以反過來思考方法應該如何設計,其規則大概是看輸入輸出,也就是有什么輸入就預期響應的輸出,要注意的是方法返回值不一定是唯一的輸出,如果方法內有數據庫等操作,寫入數據庫的內容也算一個輸出,這個時候可以考慮把數據庫的輸入反饋到方法返回值當中,總之是要驗證指定的輸入有指定的輸出。
注:調save()保存數據嚴格來說也不算業務,其只與技術有關,可以進一步從業務邏輯中剝離出來。上面主要是方便說明mock一個對象的例子。
2.3 文檔
2.3.1 Mockito
Mockito還是需要學習一下具體的用法的,重點可以先了解如何匹配參數,然后如何指定返回值(含拋異常)等,再高級的用法則可以用到再查找文檔。
Mockito各個版本的文檔列表:https://javadoc.io/doc/org.mockito/mockito-core/latest/org.mockito/org/mockito/Mockito.html
Mockito-4.5.1的詳細文檔:https://javadoc.io/doc/org.mockito/mockito-core/4.5.1/org/mockito/Mockito.html
2.3.2 AssertJ
AssertJ則通過方法名稱就大概了解其用法了,重點可以了解一下在拋異常的場景如何進行斷言。
文檔:https://assertj.github.io/doc/
3 架構一小步
規范:把業務規則抽離出來,不依賴數據庫和中間件進行詳細的單元測試。
規范:編寫可測試的代碼,如果測試代碼有邏輯則反向說明代碼可測試性不足。