iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 18
1
Modern Web

站在Web前端人員角度,學習 Spring Boot 後端開發系列 第 18

Day 18 - Spring Boot 單元測試 Service層使用Mockito

  • 分享至 

  • xImage
  •  

你不用很厲害才開始 你要開始了才會很厲害

今天跟我師父去參加GitLab CI 從團隊導入到運用 (pizza好好吃) ,了解如何使用Gitlab導入CI流程至團隊,其中有討論到Unit test的重要性,有與會的成員提出本身團隊導入單元測試會讓程式開發時間變長,而講者分享其實不能只看當下寫測試程式花費的時間,若不撰寫測試程式,未來加入功能或是重構功能,改A壞B修復它所花費的時間說不定更多。這個其實我也很有感,之前在寫專案時,因為輸出資料有變動了,我去改動function時,因為之前已經有寫測試程式,測試程式自然過不了,所以我就很快速的修復並交付功能,接下來,讓我們進入正題吧!
https://ithelp.ithome.com.tw/upload/images/20200927/20118857OXkdFf4v5y.jpg


先前我們有提到為什麼要作對程式單元測試以及好處為何,可以看這篇Day 09 - Spring Boot 基礎單元測試 ,這一篇有簡單的實作單純測試Entity 的Getter, Setter是否運作良好,今天要來測試Service層的輸入及輸出是否符合我們預期結果。

下方再一次顯示後端的架構圖,我們要測試Service層所處理後的行為是否符合我們預期結果,由於目前 Service層會呼叫Dao層來操作資料庫,所以我們會需要建立一個假的Dao物件,來模擬Dao操作資料返回的結果,單純測試Service的運作邏輯是否符合我們預期。
https://ithelp.ithome.com.tw/upload/images/20200927/20118857JW2Nvcs1WL.png

Mockito 是什麼?

Mockito 是一種 Java mock 框架,他主要就是用來做 mock 測試的,可以模擬任何 Spring 管理的 bean、模擬方法的返回值、模擬拋出異常…等,從而可以校驗出這個 mock 對象是否有被正確的順序調用,以及按照期望的參數被調用。

Mockito.when( 對象.方法名() ).thenReturn( 自定義結果 )

以下要用Mockito實作創建一個假的Dao對象,替換掉真實的Dao對象,模擬Dao的返回的數據結果,而不是真正去調用Dao操作資料庫,來快速測試當前想要測試的Service。

一樣採取3A測試原則:

  1. Arrange 初始化目標物件、相依物件、方法參數、預期結果
  2. Act 呼叫目標物件的方法
  3. Assert 驗證是否符合預期

SpringBoot 單元測試中使用 Mockito 測試Service

我們在建立Spring Boot 專案時已經引入 spring-boot-starter-test dependency時,通常包含了常用的模組 Junit、Spring Test、AssertJ、Mockito 等。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

測試Service

Spring Boot專案撰寫單元測試要先在類別前加入@SpringBootTest 測試標註。
測試Service的話,要mock Dao,那麼就要在Dao上加入@MockBean 註解,表示Mockito會幫我們創造一個假的對象,替換真實的Dao。

@SpringBootTest
public class TestTodoService {
    @Autowired
    TodoService todoService;

    @MockBean
    TodoDao todoDao;
}

測試TodoService的getTodos()方法

getTodos()方法

@Service
public class TodoService {
	@Autowired
	TodoDao todoDao;
	
	public Iterable<Todo> getTodos() {
	     return todoDao.findAll();
	 }
}

測試getTodos():

@Test
public void testGetTodos () {
    // [Arrange] 預期資料
    List<Todo> expectedTodosList = new ArrayList();
    Todo todo = new Todo();
    todo.setId(1);
    todo.setTask("洗衣服");
    todo.setStatus(1);
    expectedTodosList.add(todo);

    // 定義模擬呼叫todoDao.findAll() 要回傳的預設結果
    Mockito.when(todoDao.findAll()).thenReturn(expectedTodosList);

    // [Act]操作todoService.getTodos();
    Iterable<Todo> actualTodoList = todoService.getTodos();

    // [Assert] 預期與實際的資料
    assertEquals(expectedTodosList, actualTodoList);
 }

測試TodoService的createTodo()方法

createTodo()方法

@Service
public class TodoService {
	@Autowired
	TodoDao todoDao;
	
    public Integer createTodo(Todo todo) {
        Todo rltTodo = todoDao.save(todo);
        return rltTodo.getId();
    }
}

測試createTodo()

@Test
public void testCreateTodo () {
    // [Arrange] 準備資料
    Todo todo = new Todo();
    todo.setId(1);
    todo.setTask("寫鐵人賽文章");
    todo.setStatus(1);

    // 模擬呼叫todoDao.save(todo) 的回傳結果
     Mockito.when(todoDao.save(todo)).thenReturn(todo);

     // [Act] 實際呼叫操作todoService.createTodo
    Integer actualId = todoService.createTodo(todo);

    //  [Assert] 預期與實際的資料
    assertEquals(1, actualId);
}

測試TodoService的updateTodo()方法

updateTodo()方法

public Boolean updateTodo(Integer id,Todo todo) {
        Optional<Todo> isExistTodo = findById(id);
        if (! isExistTodo.isPresent()) {
            return false;
        }
        Todo newTodo = isExistTodo.get();
        if (todo.getStatus() == null) {
            return false;
        }
        newTodo.setStatus(todo.getStatus());
        try {
            todoDao.save(newTodo);
            return true;
        } catch (Exception e) {
            return false;
        }
  }

測試updateTodo(),有三種情境:

  1. update Todo 成功

  2. update Todo 但找不到對應的id

  3. update Todo 出現例外

@Test
public void testUpdateTodoSuccess () {
    // 準備資料
    Todo todo = new Todo();
    todo.setId(1);
    todo.setTask("寫鐵人賽文章");
    todo.setStatus(1);
    Optional<Todo> resTodo = Optional.of(todo);

    // 模擬呼叫todoDao.findById(id) 回傳的資料
    Mockito.when(todoDao.findById(1)).thenReturn(resTodo);

    // [Arrange] 更改的資料
    todo.setStatus(2);

    // [Act] 實際呼叫操作todoService.createTodo
    Boolean actualUpdateRlt = todoService.updateTodo(1, todo);

    //  [Assert] 預期與實際的資料
    assertEquals(true, actualUpdateRlt);
}

@Test
public void testUpdateTodoNotExistId () {
    // 準備更改的資料
    Todo todo = new Todo();
    todo.setStatus(2);
    Optional<Todo> resTodo = Optional.of(todo);

    // 模擬呼叫todoDao.findById(id),資料庫沒有id=100的資料 回傳empty物件
    Mockito.when(todoDao.findById(100)).thenReturn(Optional.empty());

    // [Act] 實際呼叫操作todoService.updateTodo()
    Boolean actualUpdateRlt = todoService.updateTodo(100, todo);

    // [Assert] 預期與實際的資料
    assertEquals(false, actualUpdateRlt);
}

@Test
public void testUpdateTodoOccurException () {
    // 準備更改的資料
    Todo todo = new Todo();
    todo.setId(1);
    todo.setStatus(1);
    Optional<Todo> resTodo = Optional.of(todo);

    // 模擬呼叫todoDao.findById(id),資料庫有id=1的資料
    Mockito.when(todoDao.findById(1)).thenReturn(resTodo);
    todo.setStatus(2);

    // 模擬呼叫todoDao.save(todo)時發生NullPointerException例外
    doThrow(NullPointerException.class).when(todoDao).save(todo);
    
    // [Act] 實際呼叫操作todoService.updateTodo()
    Boolean actualUpdateRlt = todoService.updateTodo(100, todo);

    //  [Assert] 預期與實際的資料
    assertEquals(false, actualUpdateRlt);
}

測試TodoService的deleteTodo()方法

deleteTodo() 方法,有三種情境:

  1. delete Todo 成功

  2. delete Todo 但找不到對應的id

  3. delete Todo 出現例外

@Service
public class TodoService {
	@Autowired
	TodoDao todoDao;

	public Boolean deleteTodo(Integer id) {
     Optional<Todo> findTodo = findById(id);
      if (!findTodo.isPresent()) {
          return false;
      }
      try {
          todoDao.deleteById(id);
          return true;
      } catch (Exception e) {
          return false;
      }
	}
}

測試deleteTodo() 方法

@Test
public void testDeleteTodoSuccess () {
    //準備更改的資料
    Todo todo = new Todo();
    todo.setId(1);
    todo.setTask("鐵人賽文章");
    todo.setStatus(2);
    Optional<Todo> resTodo = Optional.of(todo);

    // 模擬呼叫todoDao.findById(id),模擬資料庫有id=1的資料
    Mockito.when(todoDao.findById(1)).thenReturn(resTodo);

    // [Act] 實際呼叫操作todoService.deleteTodo()
    Boolean actualDeleteRlt = todoService.deleteTodo(1);

    //  [Assert] 預期與實際的資料
    assertEquals(true, actualDeleteRlt);
 }

@Test
public void testDeleteTodoIdNotExist () {
    //準備更改的資料
    Todo todo = new Todo();
    todo.setId(1);
    todo.setTask("鐵人賽文章");
    todo.setStatus(2);
    Optional<Todo> resTodo = Optional.of(todo);

    // 模擬呼叫todoDao.findById(id),並模擬資料庫沒有id=100的資料
    Mockito.when(todoDao.findById(100)).thenReturn(Optional.empty());

    // [Act] 實際呼叫操作todoService.deleteTodo()
    Boolean actualDeleteRlt = todoService.deleteTodo(100);

    //  [Assert] 預期與實際的資料
    assertEquals(false, actualDeleteRlt);
}

@Test
public void testDeleteTodoOccurException () {
    //準備更改的資料
    Todo todo = new Todo();
    todo.setId(1);
    todo.setTask("鐵人賽文章");
    todo.setStatus(2);
    Optional<Todo> resTodo = Optional.of(todo);

    // 模擬呼叫todoDao.findById(id),並模擬資料庫有id=1的資料
    Mockito.when(todoDao.findById(1)).thenReturn(resTodo);

    // 模擬呼叫todoDao.deleteById(id),會發生NullPointerException
    doThrow(NullPointerException.class).when(todoDao).deleteById(1);

    // [Act] 實際呼叫操作todoService.deleteTodo()
    Boolean actualDeleteRlt = todoService.deleteTodo(1);

    //  [Assert] 預期與實際的資料
    assertEquals(false, actualDeleteRlt);
}

https://ithelp.ithome.com.tw/upload/images/20200927/20118857vZFtN53R3l.png

程式碼會同步到Github

Reference

Mockito
SpringBoot - 單元測試工具 Mockito


上一篇
Day 17 - Spring Boot Todo List RESTful API 實作
下一篇
Day 19 - Spring Boot HTTP的請求也可以模擬測試?使用MockMvc
系列文
站在Web前端人員角度,學習 Spring Boot 後端開發30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言