iT邦幫忙

2023 iThome 鐵人賽

DAY 23
0
自我挑戰組

Techschool Goalng Backend Master Class 的學習記錄系列 第 24

[Day 24] Mock DB for testing HTTP API in Go and achieve 100% coverage Part 3

  • 分享至 

  • xImage
  •  

Write unit test for Get Account API

  • Code Flow:
    1. 透過 randomAccount() 函數建立一個隨機的測試帳戶。
    2. 使用 gomock.NewController(t) 建立一個新的 gomock.Controller,此控制器允許您設置和驗證期望的 mock 方法調用。
    3. 利用剛剛建立的控制器,透過 mockdb.NewMockStore(ctrl) 創建一個 mock 的資料庫存儲。
    4. 使用 store.EXPECT().GetAccount(...) 設定一個 stub,表示當 GetAccount 方法被呼叫時,它應該使用任意的 context 和特定的 account.ID
    5. 透過 mock 的資料庫存儲,使用 NewServer(store) 建立一個模擬的 HTTP 伺服器。這個伺服器不會真的啟動,但它會模擬 API 請求和響應。
    6. 根據測試帳戶的 ID,構建一個特定的 URL 來模擬發起一個 "Get Account" 的請求。
    7. 使用 server.router.ServeHTTP(recorder, request) 處理此請求,並捕獲模擬伺服器的響應。
    8. 使用 require 函數檢查響應的狀態碼和響應主體,以確保它們與預期的結果匹配。

func TestGetAccountAPI(t *testing.T) {
    account := randomAccount()

    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    store := mockdb.NewMockStore(ctrl)
    store.EXPECT().
        GetAccount(gomock.Any(), gomock.Eq(account.ID)).
        Times(1).
        Return(account, nil)

    server := NewServer(store)
    recorder := httptest.NewRecorder()

    url := fmt.Sprintf("/accounts/%d", tc.accountID)
    request, err := http.NewRequest(http.MethodGet, url, nil)
    require.NoError(t, err)

    server.router.ServeHTTP(recorder, request)
    require.Equal(t, http.StatusOK, recorder.Code)
    requireBodyMatchAccount(t, recorder.Body, account)
}
  • Get Account API 單元測試

    1. 創建一個新的檔案 account_test.go 在 api package 內。
    2. 定義一個新函式 TestGetAccountAPI() 採用 testing.T 輸入參數。
    func TestGetAccountAPI(t *testing.T) {
    }
    
  • 隨機產生賬戶

    1. 創建一個 randomAccount() 函數來生成隨機的賬戶。
    2. 該賬戶的 ID 是 1 到 1000 之間的隨機整數,Owner 使用 util.RandomOwner(),Balance 使用 util.RandomMoney(),Currency 使用 util.RandomCurrency()
    func randomAccount() db.Account {
        return db.Account{
            ID:       util.RandomInt(1, 1000),
            Owner:    util.RandomOwner(),
            Balance:  util.RandomMoney(),
            Currency: util.RandomCurrency(),
        }
    }
    
  • 使用 MockStore 進行測試

    1. 使用 randomAccount() 生成新賬戶。
    2. 創建一個新的 mock controller 使用 gomock.NewController,並傳入 testing.T 物件。
    3. 使用 defer ctrl.Finish() 確保 controller 的結束方法被呼叫。
    4. 使用 mockdb.NewMockStore(ctrl) 創建一個新的 mock store。
    func TestGetAccountAPI(t *testing.T) {
        account := randomAccount()
    
        ctrl := gomock.NewController(t)
        defer ctrl.Finish()
    
        store := mockdb.NewMockStore(ctrl)
    }
    
  • 建立 mock store 的 stubs

    1. 使用 store.EXPECT().GetAccount() 創建 stub。
    2. 預期此函數會使用任何 context 和特定的 account ID 被呼叫。
    3. 使用 Times(1) 確定這個函數只被呼叫一次。
    4. 使用 Return(account, nil) 指定當 GetAccount() 被呼叫時返回的值。
    func TestGetAccountAPI(t *testing.T) {
        account := randomAccount()
    
        ctrl := gomock.NewController(t)
        defer ctrl.Finish()
    
        store := mockdb.NewMockStore(ctrl)
        store.EXPECT().
            GetAccount(gomock.Any(), gomock.Eq(account.ID)).
            Times(1).
            Return(account, nil)
    }
    
  • 建立和測試 HTTP 伺服器

    1. 使用 NewServer(store) 創建新伺服器。
    2. 使用 httptest.NewRecorder() 創建新的 ResponseRecorder。
    3. 定義要呼叫的 API 的 URL 路徑。
    4. 創建一個新的 GET 請求到該 URL。
    5. 送出請求並檢查回應。
    func TestGetAccountAPI(t *testing.T) {
        ...
    
        server := NewServer(store)
        recorder := httptest.NewRecorder()
    
        url := fmt.Sprintf("/accounts/%d", tc.accountID)
        request, err := http.NewRequest(http.MethodGet, url, nil)
        require.NoError(t, err)
    }
    
  • 擴充測試

    1. 除了檢查 HTTP 狀態碼,也應檢查回應主體。
    2. 創建一個新函數 requireBodyMatchAccount() 來比較回應主體和期望的 account 物件。
    3. 利用 ioutil.ReadAll() 從回應中讀取所有數據。
    4. 使用 json.Unmarshal 將數據解析為 account 物件,然後與期望的 account 物件進行比較。
    func requireBodyMatchAccount(t *testing.T, body *bytes.Buffer, account db.Account) {
        data, err := ioutil.ReadAll(body)
        require.NoError(t, err)
    
        var gotAccount db.Account
        err = json.Unmarshal(data, &gotAccount)
        require.NoError(t, err)
        require.Equal(t, account, gotAccount)
    }
    

Q & A:

  1. gomock.Eq(account.ID)作用是什麼呢 能直接使用account.ID ?
    1. gomock.Eq(account.ID) 是 GoMock 的一個匹配器。它用於檢查傳遞給 mock 方法的參數是否等於指定的值。在這個情境中,
    2. 為什麼不直接使用 account.ID?這是因為 GoMock 需要一種方式來描述和比較期望的參數。匹配器提供了一個彈性的方法來描述這些期望,它不僅僅限於等於某個值,還可以是一個範圍、一個條件等等。例如,你可以有 gomock.Any() 表示你不關心特定的值,只是期望該方法被調用。
    3. 所以,當你使用 gomock.Eq(account.ID),你告訴 GoMock:「我期望 GetAccount 方法被調用時,它的 ID 參數必須是account.ID」。這給了你更多的控制權和描述能力,使得 mock 更具有描述性和彈性。
  2. gomock.NewController 是什麼?
    1. 在程式測試中,我們經常需要模擬(或稱"假裝")某些部件,以便能夠在不涉及實際系統的情況下進行測試。例如,你可能想測試某個功能,但不想真的去連接資料庫。
    2. gomock.NewController 就像是GoMock中一個指揮官,他管理和監控這些模擬部件的行為。
    3. 具體來說:
      1. gomock.NewController 幫助我們建立和跟踪模擬部件。
      2. 它確保模擬部件被呼叫的方式與我們在測試中定義的方式相匹配。
      3. 如果模擬部件的行為與預期不符,它會通知我們。

上一篇
[Day 23] Mock DB for testing HTTP API in Go and achieve 100% coverage Part 2
下一篇
[Day 25] Mock DB for testing HTTP API in Go and achieve 100% coverage Part 4
系列文
Techschool Goalng Backend Master Class 的學習記錄31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言