나는 저번 달까지만 해도 테스트 코드를 잘못 작성하고 있었다.
CTO님이 내 서비스 레이어의 테스트 코드를 보더니, 이건 단위 테스트가 아니라고 하셨다.
통합 테스트 코드에 가깝기 때문에 그냥 지우는 게 낫다고 하셨다.
내 서비스 레이어 테스트 코드는 서비스부터 리포지토리까지 비즈니스 로직 전체와 CRUD까지 테스트하고 있었다.
그러나 서비스 레이어 테스트 코드는 의존성이 잘 작동하는 지만 테스트하면 된다고 하셨다.
계층마다 테스트 코드가 있다면, 컨트롤러
는 리퀘스트가 맞는 지만 테스트하고,리포지토리
는 인메모리 DB에서 CRUD가 잘 작동하는 지만 테스트, 서비스
는 의존성이 잘 작동하는 지 정도만 테스트하면 된다고 하셨다.
기획이 추가됨에 따라 비즈니스 로직도 점차 복잡해졌다. 그렇게 되면서 비즈니스 로직과 관련된다고 생각한 영역인 서비스 레이어가 점차 비대해졌다. 서비스 레이어에 전부 비즈니스 로직을 추가했기 때문이다. ^^;
이렇게 되니, 서비스 레이어에 의존성 관리 + 영속성 호출 + 비즈니스 로직 등이 혼재하게 되었다. 그에 따라 테스트 코드 역시 엉망이 되었다.
다음은 잘못 작성된 테스트 코드의 예시다. (모든 코드의 이름 등은 일부러 변경했다.) 비즈니스 로직을 테스트하기 위해 서비스 레이어 테스트 코드에 너무 많은 의존성을 주입하고 있었다.
type VVVServiceTestSuite struct {
suite.Suite
client *ent.Client
ctx context.Context
xxxService XXXService
yyyService YYYService
aaaRepository repository.AAARepository
bbbRepository repository.BBBRepository
cccRepository repository.CCCRepository
}
func (suite *VVVServiceTestSuite) SetupSuite() {
suite.client = enttest.Open(
suite.T(), "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1",
enttest.WithMigrateOptions(migrate.WithGlobalUniqueID(true)),
)
suite.ctx = context.Background()
suite.xxxService = NewXXXService()
suite.yyyService = NewYYYService()
suite.aaaRepository = repository.NewAAARepository()
suite.bbbRepository = repository.NewBBBRepository()
suite.cccRepository = repository.NewCCCRepository()
}
func (suite *VVVServiceTestSuite) TestWrong() {
// repository에 실제로 CRUD 활용해서 given 만들기
// 서비스 로직을 실제로 실행해서 when 만들기
// when의 결과를 then의 assert로 테스트하기
}
결국 컨트롤러
- 서비스
- 리포지토리
3계층만으로는, 복잡한 비즈니스 로직까지 담기엔 한계가 있음을 알게 되었다. 특히 서비스 계층이 너무 복잡해서 테스트 코드를 작성하기도 까다로워졌다.
그래서 순수한 비즈니스 로직만 담당하는 도메인
계층을 추가하였다.
다음은 내가 정리한 레이어 규칙이다.
만약 비즈니스 로직이 간단하거나 필요 없는 경우에는 기존 3 tier로만 해도 충분하다. 하지만 비즈니스 로직이 복잡할 경우에는 도메인 레이어를 추가해서 작업을 진행했다.
그에 따라 서비스 레이어가 날씬해졌다.
도메인 계층에는 어떠한 의존성도 없이 순수한 비즈니스 로직만 작성하도록 했다. 파라미터 등 메서드 시그니쳐에 ent
, grpc
등 의존성이 없도록 작성했다.
아래 코드를 사용하는 서비스 레이어는 도메인 코드의 결과를 받은 후, 트랜잭션 내에서 그 결과를 영속화하는 로직을 담고 있다.
package domain
import "github.com/go-faster/errors"
type QuizResult int
const (
Fail QuizResult = iota //Enum
Pass
)
type IQuizEvaluator interface {
Score(isCorrectList []bool) (QuizResult, error)
}
const (
QuizExamLength = 5
PassingScore = 4
)
type QuizEvaluator struct {
}
func NewQuizEvaluator() *QuizEvaluator {
return &QuizEvaluator{}
}
func (b *QuizEvaluator) Score(isCorrectList []bool) (
QuizResult,
error,
) {
if len(isCorrectList) != QuizExamLength {
return Fail, errors.Errorf(
" 퀴즈는 %v 개만큼 가능합니다.",
QuizExamLength,
)
}
count := 0
for _, isCorrect := range isCorrectList {
if isCorrect {
count++
}
}
if count >= PassingScore {
return Pass, nil
} else {
return Fail, nil
}
}
테스트 코드에도 따로 의존성이 필요없어졌다.
package domain
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestQuizScorer(t *testing.T) {
evaluator := NewQuizEvaluator()
t.Run(
"퀴즈는 5개 출제되어야 합니다", func(t *testing.T) {
//given
isCorrectList := []bool{false, true, false}
//when
result, err := evaluator.Score(isCorrectList)
//then
assert.Equal(t, result, Fail)
assert.Error(t, err)
},
)
t.Run(
"퀴즈 fail 케이스. 5개중 4개 이상 맞아야 한다", func(t *testing.T) {
//given
isCorrectList := []bool{true, true, false, false, true}
//when
result, err := evaluator.Score(isCorrectList)
//then
assert.Equal(t, result, Fail)
assert.NoError(t, err)
},
)
t.Run(
"퀴즈 pass 케이스. 5개중 4개 이상 맞아야 한다", func(t *testing.T) {
//given
isCorrectList := []bool{true, true, false, true, true}
//when
result, err := evaluator.Score(isCorrectList)
//then
assert.Equal(t, result, Pass)
assert.NoError(t, err)
},
)
}
단위 테스트와 DDD 등 공부할 거 참 많다