你不用很厲害才開始 你要開始了才會很厲害
今天跟我師父去參加GitLab CI 從團隊導入到運用 (pizza好好吃) ,了解如何使用Gitlab導入CI流程至團隊,其中有討論到Unit test的重要性,有與會的成員提出本身團隊導入單元測試會讓程式開發時間變長,而講者分享其實不能只看當下寫測試程式花費的時間,若不撰寫測試程式,未來加入功能或是重構功能,改A壞B修復它所花費的時間說不定更多。這個其實我也很有感,之前在寫專案時,因為輸出資料有變動了,我去改動function時,因為之前已經有寫測試程式,測試程式自然過不了,所以我就很快速的修復並交付功能,接下來,讓我們進入正題吧!
先前我們有提到為什麼要作對程式單元測試以及好處為何,可以看這篇Day 09 - Spring Boot 基礎單元測試 ,這一篇有簡單的實作單純測試Entity 的Getter, Setter是否運作良好,今天要來測試Service層的輸入及輸出是否符合我們預期結果。
下方再一次顯示後端的架構圖,我們要測試Service層所處理後的行為是否符合我們預期結果,由於目前 Service層會呼叫Dao層來操作資料庫,所以我們會需要建立一個假的Dao物件,來模擬Dao操作資料返回的結果,單純測試Service的運作邏輯是否符合我們預期。
Mockito 是一種 Java mock 框架,他主要就是用來做 mock 測試的,可以模擬任何 Spring 管理的 bean、模擬方法的返回值、模擬拋出異常…等,從而可以校驗出這個 mock 對象是否有被正確的順序調用,以及按照期望的參數被調用。
Mockito.when( 對象.方法名() ).thenReturn( 自定義結果 )
以下要用Mockito實作創建一個假的Dao對象,替換掉真實的Dao對象,模擬Dao的返回的數據結果,而不是真正去調用Dao操作資料庫,來快速測試當前想要測試的Service。
一樣採取3A測試原則:
我們在建立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>
Spring Boot專案撰寫單元測試要先在類別前加入@SpringBootTest
測試標註。
測試Service的話,要mock Dao,那麼就要在Dao上加入@MockBean
註解,表示Mockito會幫我們創造一個假的對象,替換真實的Dao。
@SpringBootTest
public class TestTodoService {
@Autowired
TodoService todoService;
@MockBean
TodoDao todoDao;
}
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);
}
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);
}
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(),有三種情境:
update Todo 成功
update Todo 但找不到對應的id
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);
}
deleteTodo() 方法,有三種情境:
delete Todo 成功
delete Todo 但找不到對應的id
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);
}
程式碼會同步到Github
Mockito
SpringBoot - 單元測試工具 Mockito