談一談單元測試

語言: CN / TW / HK

簡介: 對於開發人員來說,單元測試一定不會陌生,但在各種原因下會被忽視,尤其是在我接觸到的專案中,提測階段發現各種各樣的問題,我覺得有必要聊一下單元測試。為了寫而寫的單元測試沒什麼價值,但一個好的單元測試帶來的收益是非常客觀的。問題是怎麼去寫好單元測試?怎麼去驅動寫好單元測試?

作者 | 有塵

來源 | 阿里技術公眾號

寫在前面

對於我們開發人員來說,單元測試一定不會陌生,但在各種原因下會被忽視,尤其是在我接觸到的專案中,提測階段發現各種各樣的問題,我覺得有必要聊一下單元測試。

為了寫而寫的單元測試沒什麼價值,但一個好的單元測試帶來的收益是非常客觀的。問題是怎麼去寫好單元測試?怎麼去驅動寫好單元測試?

一 我們的現狀

現狀一:多個專案完全沒有單元測試。

現狀二:開發人員沒有寫單元測試的習慣,或者由於趕業務記錄而沒有時間去寫。

現狀三:單元測試寫成了整合測試,比如容器、資料庫,導致單元測試執行時間長,失去了意義。

現狀四:太依賴整合測試。

以上是我在aone找的兩個專案的測試情況,基本不考慮單元測試就合併釋出,形同虛設。

站在開發的角度講,導致以上問題的原因大概有以下幾點:

1、開發成本

對於系統初期,可能要花很多時間去寫新業務,對於老系統又太過龐大,無法下手。

2、維護成本

每修改相關的類,或者重構一次程式碼,我們就要去修改相應的單元測試。

3、ROI

投入產出是不是正收益?可能無論是管理者還是我們開發自己都回質疑這個問題,所以有時候沒有強有力的動力。

二 怎麼解決

說來說去都是成本的問題,所以我們怎麼去解決成本呢?

那麼,我們一切從最開始說起:開發的成本

一個單元測試的傳統寫法,包含以下幾個方面:

  1. 測試資料 (被測資料,和依賴物件)
  2. 測試方法
  3. 返回值斷言
@Test
  public void testAddGroup() {
    // 資料
    BuyerGroupDTO groupDTO = new BuyerGroupDTO();
    groupDTO.setGmtCreate(new Date());
    groupDTO.setGmtModified(new Date());
    groupDTO.setName("中國");
    groupDTO.setCustomerId(customerId);
    // 方法
    Result<Long> result = customerBuyerDomainService.addBuyerGroup(groupDTO);
    // 返回值斷言
    Assert.assertTrue(result.isSuccess());
    Assert.assertNotNull(result.getData());
  }

一個簡單的測試還好,但如果是一邏輯複雜,且入引數據複雜的時候,那寫起來其實挺頭痛的。怎麼解放我們程式設計師的雙手?

“工欲善其事必先利其器”

我們以最大的努力降低我們的開發成本,這就涉及到我們測試框架和工具的選擇問題

1 測試框架選擇

首先第一個問題就是junit4和junit5的選擇,【從junit4到junit5】 我覺得最便利的一個好處就是可以引數化測試,並且基於引數化測試我們可以更加靈活的配置我們的引數。

效果如下:

@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
    assertTrue(StringUtils.isPalindrome(candidate));
}

更好的是,junit5提供了擴充套件,比如我們常用的json格式。這裡我們使用json檔案作為輸入:

@ParameterizedTest
  @JsonFileSource(resources = {"/com/cq/common/KMPAlgorithm/test.json"})
   public void test2Test(JSONObject arg) {
    Animal animal = JSONObject.parseObject(arg.getString("Animal"),Animal.class); 
    List<String> stringList = JSONObject.parseArray(arg.getString("List<String>"),String.class); 
    when(testService.testOther(any(Student.class))).thenReturn(stringArg);
    when(testService.testMuti(any(List.class),any(Integer.class))).thenReturn(stringList);
    when(testService.getAnimal(any(Integer.class))).thenReturn(animal);
    String result = kMPAlgorithm.test2();
    //todo verify the result
  }

2 mock框架

然後就是其他mock類的框架了

Mockito: 語法特別優雅,對於容器類的模擬比較合適,且對於返回值為空的函式呼叫也提供比較好的斷言。缺點是不能模擬靜態方法(3.4.x以上版本已支援)

EasyMock: 使用方法類似,但是更嚴格

PowerMock: 可以作為Mockito的一個補充,比如要測試靜態方法,不過不支援junit5

Spock: 基於Groovy語言的單元測試框架

3 資料庫層

這裡主要介紹一下H2資料庫,其基於記憶體來作為對於關係型資料庫的模擬,執行完成自動釋放,達到隔離的目的。

主要配置:ddl檔案路徑、dml檔案路徑。這裡不作詳述。

但對於要不要整合資料庫,很難去定義,它的作用主要是用來驗證sql語法的問題,但是相對來說較重,建議可以用於輕量級的整合測試。

三 Junit5和Mockito

後面講到的自動生成使用的框架和業界使用最多的都是MocKito,所以這裡重點介紹一下,包括使用時遇到的問題。

1 使用方法

分別單獨引入依賴,推薦引入最新版

  1. 使用spring-test全家桶

junit5的使用方法這裡就不多做介紹,主要說一下這個ArgumentsProvider介面,實現它就可以自定義引數化類,類似於自帶的ValueSource、EnumSource等。

2 Mockito 主要註解介紹

先問為什麼,為什麼需要Mockito

因為:現在的java專案幾乎離不開spring框架,而其最為著名的就是IOC,所有的bean用容器來管理,所以這給我們單元測試帶來一個問題,如果要對bean做單元測試,就需要啟動容器,那麼帶來的時間的開銷將會很大。所以Mockito給我門帶來了一系列的解決方法,讓我們可以輕鬆的對bean 進行測試。

假設我們要對上面的A.func()進行單元測試。

@InjectMocks註解

表示需要注入bean的類,有兩種

  1. 被測試類,這種很容易理解,我們測試這個類,當然也需要向其注入bean。比如上面的A
  2. 被測試類中的,需要執行其真實的方法,但其裡面也要主要bean,也就是上面的C,我們需要測試neeExec方法,但我們不關係B的具體細節。現實中比如事物,併發鎖等。這一類需要Mockito.spy(new C())的形式,不然會報錯

@Mock

表示要mock的資料,也就是不真實執行其方法內容,只按照我們的規則執行,或者返回,比如使用when().thenReturn()語法。

當然也可以,執行真實方法,則需要when().thenCallRealMethod()方式。

@Spy

表示所有方法都走真實方式,比如有些工具類,轉換類,我們也寫成了bean的形式(嚴格來說這種需要寫成靜態工具類)。

@ExtendWith(MockitoExtension.class)
public class ATest  {
  @InjectMocks
  private A a=new A(); 
  @Mock
  private B b;
  @Spy
  private D d;
  @InjectMocks
  private C c= Mockito.spy(new C());;

  @BeforeEach
  public void setUp() throws Exception {
    MockitoAnnotations.openMocks(this);
  }
  @ParameterizedTest
  @ValueSource(strings = {"/com/alibaba/cq/springtest/jcode5test/needMockService/A/func.json"})
   public void funcTest(String str) {
    JSONObject arg= TestUtils.getTestArg(str);
    a.func();
    //todo verify the result
  }

}

3 Mockito和junit5常見問題

mock靜態方法

mockito3.4以後開始支援,之前的版本可以使用PowerMock輔助使用

Mockito版本和java版本相容問題

報錯如下

Mockito cannot mock this class: xxx
Mockito can only mock non-private & non-final classes.

原因是2.17.0及之前的版本與java8是相容的

但2.18之後需要使用java11,為了在java8中使用Mockito,則需要引入另一個包

Jupiter-api版本相容問題

Process finished with exit code 255
java.lang.NoSuchMethodError: org.junit.jupiter.api.extension.ExtensionContext.getRequiredTestInstances()Lorg/junit/jupiter/api/extension/TestInstance

第一個問題是因為junit5中api、engine、params版本不一致導致的。

第二個問題是因為jupiter-api版本太低的問題,5.7.0以後的版本才支援。

四 測試程式碼自動生成

選好了框架,我們還是沒有解決我們的問題,“怎麼節約開發成本?” ,這一節我們來談這個問題,這也是我主要想表達的。

對於寫單元測試,一直以來是比較頭痛的事情,要組裝各種各樣的資料,可能還沒跑成功,就被一堆“xxxx不能為null”的報錯搞煩了。因此我們有理由去設想,有沒有辦法去解決這件事情。

點選連結檢視原文,關注公眾號【阿里技術】獲取更多福利! https:// mp.weixin.qq.com/s/ioya 1kzdTGPB0oOZ3DUmig

版權宣告: 本文內容由阿里雲實名註冊使用者自發貢獻,版權歸原作者所有,阿里雲開發者社群不擁有其著作權,亦不承擔相應法律責任。具體規則請檢視《阿里雲開發者社群使用者服務協議》和《阿里雲開發者社群智慧財產權保護指引》。如果您發現本社群中有涉嫌抄襲的內容,填寫侵權投訴表單進行舉報,一經查實,本社群將立刻刪除涉嫌侵權內容。