테스트 코드란 어떻게 작성하는 것일까 (현재 진행형 고민)

dasd412·2024년 12월 22일
0

실무 문제 해결

목록 보기
12/17

CTO님의 피드백

나는 저번 달까지만 해도 테스트 코드를 잘못 작성하고 있었다.
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계층만으로는, 복잡한 비즈니스 로직까지 담기엔 한계가 있음을 알게 되었다. 특히 서비스 계층이 너무 복잡해서 테스트 코드를 작성하기도 까다로워졌다.

그래서 순수한 비즈니스 로직만 담당하는 도메인 계층을 추가하였다.
다음은 내가 정리한 레이어 규칙이다.

레이어 규칙

  • 컨트롤러 레이어 : 서비스로 들어가기 전 리퀘스트의 유효성 검사
  • 서비스 레이어 : grpc, entgo 등의 여러 가지 의존성을 관리하고 로직의 순서를 결정 및 트랜잭션을 관리
  • 도메인 레이어 : 실제 비스니즈 로직이 작동하는 영역. ent나 graphql, grpc 등 의존성이 들어가선 안됨. 반드시 plain하게 만들어야 함. 그래야만 테스트하기 쉬움.
  • 리포지토리 레이어 : 레디스, ent 등 영속성이 필요한 영역.

만약 비즈니스 로직이 간단하거나 필요 없는 경우에는 기존 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 등 공부할 거 참 많다


profile
시스템 아키텍쳐 설계에 관심이 많은 백엔드 개발자입니다. (Go/Python/MSA/graphql/Spring)

0개의 댓글