Golang Test Standard
golang application 개발팀에서 테스트에 대한 가이드가 필요하다고 요청받아 찾아본 내용을 기술한다.
좋은 사례로 Thanos Teams, Uber, Devocean, Buzzvil 의 내용을 참고하였다.
Test Code의 표준화의 목적은 테이블 주도 테스트(Table-driven Test)로 개발자가 함수를 개발하며 여러 테스트 케이스를 수행해보며 중복을 제거할 수 있도록 표준을 정의한다.
먼저 테스트의 범위를 생각하면 아래 3가지의 항목이 존재한다.
1. Data Access Layer - 데이터베이스와의 상호작용, SQL 쿼리 검증
2. Business Layer - 비즈니스 로직이 의도한대로 작동하는가, 외부 의존성
3. Presentation Layer - API Endpoint 또는 Controller, Handler
로직에 대한 검증이 대부분이라 Business Layer를 주로 테스트 한다.
Library
제일 많이 사용되는 라이브러리는 testify 이다. 하지만 메소드 명을 문자열로 받아 type safe 하게 코드를 작성할 수 없다. 그리고 함께 붙어 있는 mockery는 Mocking 기법(가짜모듈)인데, 인터페이스를 읽어 자동으로 mock 객체를 생성해 준다.
그 다음으로 핫한 gomock 을 사용해보기로 했다. 이건 type safe 하게 코드를 작성할 수 있다.(타입을 기반한 메소드 자동완성)
참고로 gomock에서도 Mocking을 자동으로 해주는 mockgen이라는 라이브러리가 존재한다.
의존성 주입 및 변수 초기화 방법
// 1. 의존성 주입 형태 - Good
func NewUserService(userRepository UserRepository) *UserService{
return &UserService{
userRepository: userRepository,
}
}
// 2. 의존성을 주입하지 않는 형태 - Bad
func NewUserServiceWithoutInjection(client *ent.UserClient) *UserService{
return &UserService{
userRepository: NewUserRepository(client),
}
}
표준화
타노스팀, 우버에서 거의 비슷한 표준규칙을 가지고 있어 거의 그대로 가져왔다.
기본 작성 규칙
- 파일명은 _test.go 형식을 따른다.
- 테스트 대상 패키지에 _test 를 붙여서 테스트를 작성한다(Blackbox 테스트)
- 함수명은 맨 앞에 Test 를 붙인다.
- 매개변수는 t *testing.T 를 받는다.
- 실패지점에서 t.Fail() 을 호출한다.
파일 | controller.go | controller_test.go |
패키지 | package articlesvc | package articlsvc_test |
함수 | GetArticles() | TestGetArticles() |
구조체 슬라이스를 tests라고 하고, 각 테스트 케이스를 tt라고 한다. 또한 각 테스트 케이스의 입력 및 출력 값을 give 및 want 접두어를 사용하여 설명(explicating)하는 것을 권장한다.
tests := []struct{
give string
wantHost string
wantPort string
}{
// ...
}
for _, tt := range tests {
// ...
}
Mocking 예제
테스트할 로직 : 숫자를 넘겨받아 더해서 출력
- 모듈 인터페이스 파일 - doer.go
- 모듈 로직 구현 파일 - test.go
- 실제 메소드 구현 파일 - user.go
- _test 파일 - user_test.go
package repository
// doer.go
type Doer interface {
MyFuncPlus(int) int
}
인터페이스 파일을 생성했으면 mockgen 라이브러리로 mock 객체를 생성해 준다.(mock 폴더아래)
mockgen -source=./repository.go -destination=./mock/mock_repository.go -package=mock
package repository
// test.go
func MyFuncPlus(limit int) int {
return limit + 22
}
package repository
// user.go
type DoerUser struct {
Doer Doer
}
func (u *DoerUser) DoerUse(limit int) int {
return u.Doer.MyFuncPlus(limit)
}
package repository_test
import (
"testing"
repository "github.com/{myRepo}/repo"
mock "github.com/{myRepo}/repo/mocks"
"github.com/stretchr/testify/assert"
"github.com/golang/mock/gomock"
)
// Mocking Test
func TestMyFuncPlus(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
tests := []struct {
name string
giveLimit int
wantExpected int
}{
{
name: "valid case",
giveLimit: 50,
wantExpected: 72,
},
}
// Mock repository 설정
mockRepo := mock.NewMockDoer(ctrl)
testUser := &repository.DoerUser{Doer: mockRepo}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 기대하는 동작을 설정
mockRepo.EXPECT().
MyFuncPlus(tt.giveLimit).
Return(tt.wantExpected).
Times(1)
// 테스트 진행
result := testUser.DoerUse(tt.giveLimit)
// 결과 확인
assert.Equal(t, tt.wantExpected, result)
})
}
}
테스트 결과
> go test user_test.go -v -run TestMyFuncPlus
=== RUN TestMyFuncPlus
=== RUN TestMyFuncPlus/valid_case
--- PASS: TestMyFuncPlus (0.00s)
--- PASS: TestMyFuncPlus/valid_case (0.00s)
PASS
ok command-line-arguments 0.489s
마치며..
go application 개발을 시작한다면 thanos team, uber 의 github에 가서 코딩스타일, 테스트에 관한 내용을 다 읽어보길 바란다. 추가로 이번에 자료를 검토해보면서 k8s resource(nginx-ingress, api-server, calico) code를 다운로드해서 test case를 분석해보니 정말 모든 테스트 케이스를 정의를 해 놓은 것을 보았다... 언젠가 그렇게 코딩하는 날이 오기를....