這篇來談一種常見情境:呼叫的方法不回傳值,要怎麼驗證它「真的有做事」?

Query 與 Command#

程式行為大致可分為兩類:

  • Query:向某個物件詢問東西。例如向 Student 問 fullName、向 Transcript 問 averageScore。
  • Command:要某個物件去做事。例如要 Student 去 register、要 Semester 去 start、要 EmailService 去 send。

物件導向的核心思考之一是:現實怎麼做,程式就怎麼寫。生活中委託別人辦事,有時對方會回報執行情形,有時則「有事再回報」。在 Command 的設計上,常見的回應方式包括:什麼都不回、回個 boolean 表示成功與否、回個 int 表示影響筆數,再不然就是出錯時丟例外(Exception)。

Tell-Don’t-Ask#

物件導向有句設計原則:「Tell. Don’t Ask.」就是在強調這種風格。當我們把事情委託出去,就應該專注在「告訴對方該做什麼」,而不是先問東問西、再自己處理。

Tell-Don’t-Ask 帶來的副作用是:如果方法不回傳值,從外部看根本不知道內部到底做了什麼。這就需要新的測試手段——驗證行為而不是驗證回傳值。

如果受測方法至少會回個值,事後可以驗證那個值;但若是 void,就要改變思路:去檢查它有沒有實際呼叫某個依賴的方法、用什麼參數、被呼叫了幾次。這就是行為測試(Behavior Verification)的精神,與檢查回傳值的狀態測試(State Verification)不同。

不回傳的 Command 範例#

延續獎學金的情境:當申請通過時間檢查後,就把申請單存進資料庫。若這是唯一條件,程式可以這樣寫:

public class ApplyScholarshipService {

    private final ApplicationChecker checker;
    private final ApplicationRepository applicationRepository;

    public ApplyScholarshipService(ApplicationChecker checker, ApplicationRepository applicationRepository) {
        this.checker = checker;
        this.applicationRepository = applicationRepository;
    }

    public void apply(Application application) {

        if (this.checker.checkTime(application)) {
            this.applicationRepository.create(application);
        }
    }
}

邏輯非常單純:「checker 通過 ➡️ 叫 repository 把申請單存起來」。

用 verify 驗證行為#

checker 因為涉及時間不容易控制,可以用 Mockito 讓它依需求回傳 truefalse。當 checker 回 true 時,repository.create() 沒有回傳值,所以要驗證的是「它有沒有被呼叫一次」。在 Mockito 中可以使用 verify

@Test
void check_ok_then_create() {

    Application application = new Application(777L);

    ApplicationChecker checker = mock(ApplicationChecker.class);
    when(checker.checkTime(application)).thenReturn(true);

    ApplicationRepository repository = mock(ApplicationRepository.class);

    new ApplyScholarshipService(checker, repository).apply(application);

    verify(repository, times(1)).create(application);
}

這個測項證明「一旦時間檢查通過,service 確實會請 repository 建立一次申請紀錄」。

反向情境#

下一個自然要驗的情境是「checker 回傳 false 時,不應該呼叫 create」。寫法大同小異,差別只在最後改成驗證 create 從未被呼叫,例如:

verify(repository, times(0)).create(application);

或使用 verify(repository, never()).create(any())。透過這兩個測項,無論 checker 結果為何,service 的行為都會被約束住。

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10260724