Skip to content

ghkdqhrbals:mockdb

Hwangbo Gyumin edited this page May 29, 2022 · 3 revisions

sqlc로 쿼리문 인터페이스 생성

type Querier interface {
    AddAccountBalance(ctx context.Context, arg AddAccountBalanceParams) (Account, error)
    CreateAccount(ctx context.Context, arg CreateAccountParams) (Account, error)
    CreateEntry(ctx context.Context, arg CreateEntryParams) (Entry, error)
    CreateTransfer(ctx context.Context, arg CreateTransferParams) (Transfer, error)
    DeleteAccount(ctx context.Context, id int64) error
    GetAccount(ctx context.Context, id int64) (Account, error)
    GetAccountForUpdate(ctx context.Context, id int64) (Account, error)
    GetEntry(ctx context.Context, id int64) (Entry, error)
    GetTransfer(ctx context.Context, id int64) (Transfer, error)
    ListAccounts(ctx context.Context, arg ListAccountsParams) ([]Account, error)
    ListEntries(ctx context.Context, arg ListEntriesParams) ([]Entry, error)
    ListTransfers(ctx context.Context, arg ListTransfersParams) ([]Transfer, error)
    UpdateAccount(ctx context.Context, arg UpdateAccountParams) (Account, error)
}

sqlc의 emit_interface=true를 통해 query의 account.go, transfer.sql, entry.sql의 인터페이스를 생성.

mockgen으로 기본함수생성

type Store interface {
	Querier // 인터페이스
	TransferTx(ctx context.Context, arg TransferTxParams) (TransferTxResult, error)
}

이후 simplebank/ghkdqhrbals/db/sqlc/store.go에 Store 인터페이스 정의 이 인터페이스는 의존성을 제거한 db테스트에 사용

mockgen -destination db/mock/store.go github.com/ghkdqhrbals/simplebank/db/sqlc Store

mockgen을 통해 앞서 정의한 Store 인터페이스를 받아오고, 가상으로 실행하는 함수를 자동으로 정의.

mock DB

func (server *Server) getAccount(ctx *gin.Context) {
	~
	account, err := server.store.GetAccount(ctx, req.ID)
	~
}
type Querier interface {
	~
	GetAccount(ctx context.Context, id int64) (Accounts, error)
	~
}

store의 GetAccount는 다음과 같음. 즉, Accounts 구조와 에러를 반환.

func (q *Queries) GetAccount(ctx context.Context, id int64) (Accounts, error) {
	row := q.db.QueryRowContext(ctx, getAccount, id)
	var i Accounts
	err := row.Scan(
		&i.ID,
		&i.Owner,
		&i.Balance,
		&i.Currency,
		&i.CreatedAt,
	)
	return i, err
}

Unit Test

testCases := []struct {
		name          string
		accountID     int64
		buildStubs    func(store *mockdb.MockStore)
		checkResponse func(t *testing.T, recorder *httptest.ResponseRecorder)
	}{
		{
			name:      "OK",
			accountID: account.ID,
			buildStubs: func(store *mockdb.MockStore) {
				store.EXPECT().
					// mock으로 테스트할 function 이름.
					GetAccount(gomock.Any(), account.ID). // 특정 ID를 argument로 받는 store.GetAccount함수가 call 되길 예상함.
					Times(1).                             // 위의 함수가 call 되는 횟수 예상
					Return(account, nil)                  // test function의 return 값 예상.
			},
			checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) {
				require.Equal(t, http.StatusOK, recorder.Code) // 200 http Response 반환
				requireBodyMatchAccount(t, recorder.Body, account)
			},
		},
		{
			name:      "NotFound",
			accountID: int64(10002),
			buildStubs: func(store *mockdb.MockStore) {
				store.EXPECT().
					GetAccount(gomock.Any(), int64(10002)). // 특정 ID를 argument로 받는 store.GetAccount함수가 call 되길 예상함.
					Times(1).                               // 위의 함수가 call 되는 횟수 예상
					Return(db.Accounts{}, sql.ErrNoRows)    // 빈 Accounts구조체 및 ErrNoRows 에러 반환 예상.
			},
			checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) {
				require.Equal(t, http.StatusNotFound, recorder.Code) // 404 ERROR http Response반환
			},
		},
		{
			name:      "BadReqeust",
			accountID: int64(-1),
			buildStubs: func(store *mockdb.MockStore) {
				store.EXPECT().
					GetAccount(gomock.Any(), int64(-1)).
					// 애초에 id min=1로 설정해두었기에, unmarshelling할 때 오류가 뜸으로 GetAccount가 실행이 안됨.
					//따라서 0으로 설정
					Times(0)

			},
			checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) {
				require.Equal(t, http.StatusBadRequest, recorder.Code) // 404 ERROR http Response반환
			},
		},
		{
			name:      "InternalServerError",
			accountID: account.ID,
			buildStubs: func(store *mockdb.MockStore) {
				store.EXPECT().
					GetAccount(gomock.Any(), account.ID).  // 특정 ID를 argument로 받는 store.GetAccount함수가 call 되길 예상함.
					Times(1).                              // 위의 함수가 call 되는 횟수 예상
					Return(db.Accounts{}, sql.ErrConnDone) // 빈 Accounts구조체 및 ErrNoRows 에러 반환 예상.
			},
			checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) {
				require.Equal(t, http.StatusInternalServerError, recorder.Code) // 404 ERROR http Response반환
			},
		},
		// TODO: testCase 추가
	}

	// Stubs 케이스 실행
	for i := range testCases {

		tc := testCases[i]
		fmt.Println("Total Case: ", len(testCases), "\nCase : ", tc.name, "")
		cc := tc.accountID
		t.Run(tc.name, func(t *testing.T) {
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()
			store := mockdb.NewMockStore(ctrl)
			tc.buildStubs(store)

			// start test server and send request
			server := NewServer(store)

			// request 생성
			recorder := httptest.NewRecorder()
			url := fmt.Sprintf("/accounts/%d", cc)
			request, err := http.NewRequest(http.MethodGet, url, nil)
			require.NoError(t, err)

			// send API request & response in recorder
			server.router.ServeHTTP(recorder, request)
			tc.checkResponse(t, recorder)
		})
	}
}
go test -run "path/function name" -v(detaily describe) -cover(coverage) //test

FYI

mock은 전체 테스트: coverage = 100% && 행동 관찰

stubs는 특정 기능부분 테스트: coverage <= 100% && 상태 관찰

의미는 이러하지만 사실 이 두 가지 테스트 방법은 따로 구분되지 않음. ex) mock 또한 < 100% 가능