iT邦幫忙

2024 iThome 鐵人賽

DAY 21
0

接續前一篇進行 Service 的測試,我們接序同一個情境針對 Product 的 CRUD,但是應用不同的寫法, 這邊會運用到 Mockito 這個套件來幫助我們應用 Mock 的虛構物件進行測試。

關於 Mock

  • 顧名思義 Mock 有模擬、假的意思,將這樣模擬概念應用到測試中,就是希望程式原先依賴的對象都可以透過 mock 方式建立一個假的對象去模擬真實行為。
  • 先前介紹單元測試時,有提到特性是應該各單元測試互相獨立,不依賴其他外部系統,Mock Test 是一種單元測試方法,能專注於要測試的程式本身的運作中,避免測試一個方法卻要建構整個 bean 相關的依賴架構。

一個系統架構會有多個類別之間互相依賴,且在Spring Boot 中會都註冊成為 Bean,如下圖這樣多個 Class 互相依賴,Class A 需要依賴 B 和 C

https://ithelp.ithome.com.tw/upload/images/20241001/201509778lUjZPHQvZ.png

引入 mock 測試時,就可以創建假的對象類,替換掉真實的 Class B 和 C,並可以自己設定這個 mock 對象的參數和期望結果,讓我們可以專注在測試當前的 Class A 應該要得到的回傳或是要 Pass 的結果,不受其他的外部服務影響,就能提高測試效率並專注當前測試 Class 的運作。

https://ithelp.ithome.com.tw/upload/images/20241001/20150977LbyWkqvCCD.png

Mockito 套件

Java 的 mock 測試框架, Java 中目前主流的 mock 測試工具有 Mockito、JMock、EasyMock..等, SpringBoot 目前內建的是使用 Mockito 框架。和先前 Junit 一樣,只要引用 spring-boot -starter- test 這個 dependency 就包含在裡面。

測試重點過程及用法

  • 建立模擬物件:Mockito 可以建立模擬類別,並且標示模擬類被注入位置
  • 控制行為:可定義模擬物件行為,如方法返回值、拋出異常等,且都是我們預期的行為,不須真實運作,可以避免實體運作帶來的影響(資料庫操作資料或實體當下無法運作)
  • 驗證互動:可以驗證模擬物件是如我們的預期進行控制行為,驗證互動次數、觸發順序等等。

模擬物件建立

@Mock :標註建立一個模擬物件,通常為資料庫溝通區塊(Dao)或是目前不希望調用真實實體的物件

@InjectMocks :標註模擬物件注入位置,如果今天要測試 Service 會應用到某個 Dao 來進行資料溝通,就是註記在該 Service 類別上,就會把 @Mock 的物件注入

控制行為

使用 when() 這個方法可以控制當特定方法觸發時回傳我們要的結果。

下面是幾種常用的寫法:

  • 一般驗證:

when(方法).thenReturn(自訂回傳結果)

  • 拋出 Exception 驗證:

when(方法).thenThrow(Exception Class)

  • 沒有回傳驗證:

doNothing().when(方法)

驗證互動

使用 verify() 來驗證模擬對象的方法是否按照預期被調用。

下面是幾種常用的寫法:
verify(被呼叫類, times(呼叫次數)).被呼叫類使用方法()

如果不寫 times 就是預設驗證被呼叫類呼叫使用方法一次

上面控制和驗證如果需要限制為特定值或是特定類別

when(productRepository.save(any(Product.class))).thenReturn(product);

如果 mock 會被呼叫到的方法有順序驗證,可使用 inOrder 物件來讓 mockito 依照順序驗證

先指定需要驗證的 mock 物件裝入 inOrder,再透過 inOrder.verify() 來進行,類似作法如下

InOrder inOrder = inOrder(productTestDao);

inOrder.verify(productTestDao, times(1)).findById(id);
inOrder.verify(productTestDao, times(1)).save(any(ProductTest.class));

Mockito 使用注意

  • 不能 mock 靜態方法
  • 不能 mock private 方法
  • 不能 mock final class

Mock測試應用

如果以先前我們測試 ProductService 的部分來畫一個概念圖,原本要測試需要@Autowired ProductDao,並且會直接更動資料庫,如果應用 Mock 將 ProductDao 模擬成 MockProductDao 來模擬和資料庫溝通的操作或回傳,就可以來協助我們進行這些行為的測試。

https://ithelp.ithome.com.tw/upload/images/20241001/20150977WKRi57l9ip.png

Mockito 引入測試有兩種寫法:

  1. 測試類要加上 @ExtendWith(MockitoExtension.class)
  2. 每個測試類都要初始化,所以可以用 @BeforeEach 配合初始化 MockitoAnnotations.*openMocks*(this)
// 使用 @ExtendWith 或 @BeforeEach 其中之一來初始化 MockitoExtension
// @ExtendWith(MockitoExtension.class)
public class ProductTestServiceTest {

    @Mock
    private ProductTestDao productTestDao;

    @InjectMocks
    private ProductTestService productTestService;

    @BeforeEach
    public void setup() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    public void testReadMockProduct() {
        // mockProduct
        ProductTest mockProduct = new ProductTest();
        mockProduct.setId(1L);
        mockProduct.setName("Switch 2");
        mockProduct.setPrice(11999.99);
        mockProduct.setDescription("任天堂熱門的手持遊戲機");

        when(productTestDao.findById(1L)).thenReturn(Optional.of(mockProduct));

        Optional<ProductTest> product = productTestService.getProductById(1L);
        assertTrue(product.isPresent());
        assertEquals(11999.99, product.get().getPrice());
        assertEquals("任天堂熱門的手持遊戲機", product.get().getDescription());
        verify(productTestDao, times(1)).findById(1L);
    }

    @Test
    public void testUpdateMockProduct() {
        Long id = 2L;
        ProductTest mockProduct = new ProductTest();
        mockProduct.setId(id);
        mockProduct.setName("PlayStation 5 pro discount");
        mockProduct.setPrice(21999.99);
        mockProduct.setDescription("索尼的次世代遊戲主機 - 降價促銷");

        when(productTestDao.findById(id)).thenReturn(Optional.of(mockProduct));
        when(productTestDao.save(any(ProductTest.class))).thenReturn(mockProduct);
        ProductTest updatedMockProduct = productTestService.updateProductById(id, mockProduct);

        assertNotNull(updatedMockProduct);
        assertEquals(id, updatedMockProduct.getId());
        assertEquals("PlayStation 5 pro discount", updatedMockProduct.getName());
        assertEquals(21999.99, updatedMockProduct.getPrice());

        InOrder inOrder = inOrder(productTestDao);

        inOrder.verify(productTestDao, times(1)).findById(id);
        inOrder.verify(productTestDao, times(1)).save(any(ProductTest.class));
    }

    @Test
    public void testDeleteMockProduct() {
        doNothing().when(productTestDao).deleteById(1L);
        productTestService.deleteProductById(1L);
        verify(productTestDao, times(1)).deleteById(1L);
    }

    @Test
    public void testCreateMockProduct() {
        // 新增一個新的產品
        ProductTest mockProduct = new ProductTest();
        mockProduct.setId(6L);
        mockProduct.setName("三星 Galaxy S22");
        mockProduct.setPrice(20999.99);
        mockProduct.setDescription("三星旗艦智慧型手機");

        when(productTestDao.save(mockProduct)).thenReturn(mockProduct);

        ProductTest savedProduct = productTestService.saveProduct(mockProduct);

        assertNotNull(savedProduct);
        assertNotNull(savedProduct.getId());
        assertEquals("三星 Galaxy S22", savedProduct.getName());
    }
}

Ref:

相關文章也會同步更新我的部落格,有興趣也可以在裡面找其他的技術分享跟資訊。


上一篇
Day 20 - UnitTest (4) - Service/Dao 層測試撰寫
下一篇
Day 22 - Spring Security (1) - 介紹及應用
系列文
關於我和 Spring Boot 變成家人的那件事30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言