[Golang] mock data를 이용한 test code 작성

suji·2023년 8월 21일
2

Go

목록 보기
7/9
post-thumbnail

Why?

테스트 코드는 왜 필요할까?
구글링해서 나오는 이유들 말고, 내가 몸소 느낀것만 이야기 해보자면

  • 여러가지의 test case를 계획하면서 비지니스에 대한 이해도를 높일 수 있다
  • 더 상세하게 예외처리를 생각할 수 있다
  • 휴먼에러를 크게 감축시킬 수 있다

등의 이유가 있다.
test code를 작성하는 습관만 만든다면 더욱 풍부한 개발을 할 수 있다고 장담한다.

현재 회사에서 개발하고 있는 서비스의 구조에 맞는 테스트 코드 구현 방식을 고민하고 정리해 보았다.

서비스 구조

  • app : 도메인과 인터페이스 레이어 사이의 통로 역할. 인터페이스 계층에서 도메인 계층으로 요청을 보내고 도메인 계층은 이를 처리하고 응답을 반환.
  • domain : 도메인 및 비즈니스 로직
  • infra : 외부 라이브러리 (API 포함), 데이터베이스 등 독립적으로 존재하는 모든 것으로 구성
  • interfaces : 클라이언트로부터의 요청 및 응답을 처리함
  • 참고 링크

서비스 구조에 따른 Test Code 구조

먼저, 기존에는 app 파일에서 repository 함수들을 조합하여 서비스 로직을 전체 만들었었다.

방식을 바꿔야 한다는 것을 인식하고, service 파일에서 비지니스 로직을 단위별로 함수로 만들어 app 파일에서 조합하여 완성하는 것으로 새로운 방향을 잡았다.

그래서 app단의 함수는 service단의 함수들이 모두 정상 작동 된다면 정상작동 된다고 판단할 수 있기에,

repository_test, service_test, handler_test 이렇게 3단계의 테스트 코드를 작성하기로 정하였다.

  • Repository test: DB에 잘 영속화 되었는지 확인해야하는 repository 는 gitlab 파이프라인을 통해 가상DB를 생성하여 테스트를 실행시킨다.
  • Service test: DB와의 영속성 보다는 비지니스 로직을 검증하는 것이 중요하므로 Mock 데이터를 Mocking을 이용하여 테스트를 실행한다.
  • Handler test: 통합테스트 개념으로 전체적인 작동을 확인해야하므로 가상 DB를 생성하여 테스트를 진행시킨다.

Service 단 Test Code 개발 - Testify, Mockery 사용

# cli 명령어 실행

mockery --dir=domain/repository --output=domain/repository/mocks --outpkg=mocks --all

repository mock type을 자동으로 생성하는 명령어이다.

domain/repository 위치에 mocks 폴더를 만들어 그 안에 repository interface 를 통해 mock type repository interface 를 정의한다.

# mock type repository를 의존하는 service structure 세팅

package service 

type UserServiceTestSuite struct {
	suite.Suite
	userService               *UserService
	mockUserRepository        *mocks.UserRepository
	mockTransactionRepository *mocks.TransactionRepository
}

func (ts *UserServiceTestSuite) SetupTest() {
	ts.mockUserRepository = new(mocks.UserRepository)
	ts.mockTransactionRepository = new(mocks.TransactionRepository)
	ts.userService = &UserService{
		userRepository:        ts.mockUserRepository,
		transactionRepository: ts.mockTransactionRepository}
}

func TestUserServiceTestSuite(t *testing.T) {
	suite.Run(t, new(UserServiceTestSuite))
}

# test case 작성

회원관련 서비스를 예시로 보자면,

func (ts *UserServiceTestSuite) Test_CreateARServiceUser() {
	ts.Run("사용자 생성 - 성공", func() {
		user := &entity.User{
			AccountID: uuid.NewString(),
			Email:     zero.NewString("email@email.com", true),
		}
		ts.mockUserRepository.On("FindByAccountId", mock.Anything, mock.Anything).Return(nil, gorm.ErrRecordNotFound)
		ts.mockUserRepository.On("Create", mock.Anything, mock.Anything, mock.Anything).Return(nil)
		ts.mockTransactionRepository.On("Transaction", mock.Anything).Return(nil)

		data, result := ts.userService.CreateARServiceUser(context.Background(), user.AccountID, user.Email.String)
		user.ID = data.ID
		assert.Equal(ts.T(), data, user)
		assert.Equal(ts.T(), result, dto.Success)
	})

	ts.Run("사용자 생성 - 이미 가입한 계정 - 그대로 리턴", func() {
		user := &entity.User{
			AccountID: uuid.NewString(),
			Email:     zero.NewString("email@email.com", true),
		}
		ts.mockUserRepository.On("FindByAccountId", mock.Anything, mock.Anything).Return(user, nil)
		ts.mockUserRepository.On("Create", mock.Anything, mock.Anything, mock.Anything).Return(nil)
		ts.mockTransactionRepository.On("Transaction", mock.Anything).Return(nil)

		data, result := ts.userService.CreateARServiceUser(context.Background(), user.AccountID, user.Email.String)
		user.ID = data.ID
		assert.Equal(ts.T(), data, user)
		assert.Equal(ts.T(), result, dto.Success)
	})

}

userService.CreateARServiceUser 사용자 생성하는 기능으로 내부 구성은 다음과 같다.

findByAccountId 기존 가입여부 확인 → 가입한적 없으면(nil) 회원가입 create → user 정보 리턴

  • 사용자 생성 — 성공

    • 기존에 가입한 적이 없는 경우: mockUserRepository.FindByAccountId 에서 데이터로는 nil , 에러는 gorm.ErrRecordNotFound를 반환해야한다.
    • mockUserRepository.Create 에서는 잘 생성되었다는 의미로 에러를 nil 반환해야한다.
    • → 필요한 Mock 데이터를 세팅해주고 service 함수를 실행시키면 기대하는 user 데이터와 dto.Success 응답값을 받는 것을 확인할 수 있다.
  • 사용자 생성 - 이미 가입한 경우

    • mockUserRepository.FindByAccountId 에서 데이터가 nil이 아닌 user , 에러는 nil를 반환해야한다.
profile
문제를 해결하는 백엔드 개발자

0개의 댓글