개발자의 오르막

Golang 과 TDD 본문

GoLang

Golang 과 TDD

계단 2022. 10. 2. 21:07

Keyword


TDD , golang

 

 

 

Overview


사업에 레드플레이어 라는 단어가 있다. 레드플레이어란, 특정 아이디어에 대해 반대편에 서서, 아이디어에 비관적으로 생각하고, 단점에 대해 공격적으로 어필하는 플레이어를 말한다. 팀원 모두가 긍정적인 견해로 일원화되지 않게 하기 위함이다.

TDD (Test Driven Development) 가 주는 순 기능으로 위와 같은 레드플레이어의 기능이 부각된다. 우리는 여러 테스트 코드를 짤 때, 해당 기능이 Fail 나는 상황을 여러 단계로 쪼개서 생각하고 작성한다. 여러 경우의 수가 반영된 테스트코드를 통과하면서 보다 완전한 하나의 기능이 탄생할 수 있다. 또한 하나의 기능을 여러 단위로 쪼개어 테스트를 진행 (단위테스트) 하기 때문에 우리는 손쉽게 어떤 부분이 문제인지 파악할 수 있다.

 

 

테스트 주도 개발은 어떻게 할까?


https://wooaoe.tistory.com/33

 

TDD 개발주기 Red-Green-Refactor 그림

 

 

 

테스트주도 개발이라고 해서 겁먹을 필요는 없다. 특히 나와 같은 주니어들에게 처음 테스트 주도개발에 대한 문화와 경험에 대해 얘기해보라고 하면 어떤 말을 꺼낼지 몰라 꺼려지는데 정말 단순하게 생각하면 경우의 수 생각이다.

 

 

 

 

요구사항 : 회원이 네이버, 토스, 페이코 유형 중 하나의 멤버십을 가입한다.

라는 기능을 만들고자 한다면, 위의 요구사항이 진행될 때, 어떤 경우의 수가 벌어질까에 대해 먼저 생각해보는 것이다.

우선적으로,

  • 멤버십을 생성하는 경우 (O)
  • 이미 등록된 사용자의 이름이 존재하는 경우 (X)
  • 사용자 이름을 입력하지 않은 경우 (X)
  • 멤버십 유형을 입력하지 않은 경우 (X)
  • 멤버십 유형을 입력하였지만 네이버, 토스, 페이코 에 속하지 않은 유형을 입력한 경우 (X)

로 경우의 수를 생각해볼 수 있다.

위의 경우의 수에 기반하여 우리는 기능을 먼저 만드는 것이 아니라 테스트코드를 먼저 만들면서 해당 기능에 덧붙이는 작업을 진행한다고 생각하면 된다.

 

 

 

Go 에서의 테스트 코드

t.Run("멤버십을 생성한다.", func(t *testing.T) {
		app := NewApplication(*NewRepository(map[string]Membership{}))
		req := CreateRequest{"jenny", "naver"}
		res, err := app.Create(req)
	})

	t.Run("이미 등록된 사용자 이름이 존재할 경우 실패한다.", func(t *testing.T) {
	})

	t.Run("사용자 이름을 입력하지 않은 경우 실패한다.", func(t *testing.T) {
	})

	t.Run("멤버십 타입을 입력하지 않은 경우 실패한다.", func(t *testing.T) {
	})

	t.Run("naver/toss/payco 이외의 타입을 입력한 경우 실패한다.", func(t *testing.T) {
	})

위처럼 우리는 헤딩 경우의 수를 생각해보고, 테스트 문항을 먼저 구성한 다음, 만들고자 하는 기능을 불러와서 실제 테스트코드가 동작할 수 있도록 기능을 덧붙인다는 느낌이다.

그러면 위의 TDD 개발주기 그림 처럼 먼저 TEST 를 만들고(Make), app.Create(req) 기능을 구현하면서 (Refactoring) , 테스트가 실패할 때마다(Write Failing Test) 다시 재구현하는 방식으로 덧붙여 개발한다고 생각하면 되겠다.

 

 

 

 

Go 에서 테스트 작성하기


그렇다면 Go 언어에서는 테스트코드를 어떻게 작성할까?

Go 는 간편하게 사용할 수 있는 테스트 프레임워크를 내장하고 있다. 테스트하고자 하는 특정 디렉토리 위치에서 go test 를 실행하면 *_test.go 파일들을 테스트코드로 인식하고, 이들을 일괄적으로 실행한다.

Go는 testing 이라는 표준 패키지를 사용하는데, 먼저 testing 패키지의 의존성을 주입하고, 테스트 메서드를 작성하면 된다.

 

 

  • 테스트 메서드 예시
func TestXxx(t *testing.T)

 

Go 테스트 코드 만들기 실습


그렇다면 위의 요구사항을 가져와 Go 언어로 테스트 코드를 만들면서 CreateMemberShip 기능을 구현해보자.

요구사항 : 회원이 네이버, 토스, 페이코 유형 중 하나의 멤버십을 가입한다.

 

 

0. Go 파일 생성하기

 

 

1. 요구사항에 따른 테스트 코드 생성하기

  • application_test.go
func TestCreateMembership(t *testing.T) {
	t.Run("멤버십을 생성한다.", func(t *testing.T) {
	})

	t.Run("이미 등록된 사용자 이름이 존재할 경우 실패한다.", func(t *testing.T) {
	})

	t.Run("사용자 이름을 입력하지 않은 경우 실패한다.", func(t *testing.T) {
	})

	t.Run("멤버십 타입을 입력하지 않은 경우 실패한다.", func(t *testing.T) {
	})

	t.Run("naver/toss/payco 이외의 타입을 입력한 경우 실패한다.", func(t *testing.T) {
	})
}

먼저 위의 요구사항에 맞춰서 우리가 테스트할 수 있는 경우의 수를 생각해본다. 그리고 Go 의 내장함수 testing 패키지를 사용하여 위의 코드처럼 실행시킬 수 있는 뼈대를 만든다.

 

 

 

2. 요구사항을 실행하는 코드를 생성한다.

  • application.go
type Application struct {
	repository Repository
}

func NewApplication(repository Repository) *Application {
	return &Application{repository: repository}
}

func (app *Application) Create(request CreateRequest) (CreateResponse, error) {
}

application.go 는 Reqeuset 가 들어왔을 때 기능을 수행하는 파일이다.

repository 를 구조체로 갖고 있으며, repository 에 접근하여 CreateRequest 를 통해 얻은 데이터를 넘겨주는 역할을 한다.

  • repository.go
type Repository struct {
	data map[string]Membership
}

func NewRepository(data map[string]Membership) *Repository {
	return &Repository{data: data}
}

repository.go 는 저장소의 기능을 수행하며, data 라는 map 메모리를 지니고 있다.

  • dto.go
type CreateRequest struct {
	UserName       string `json:"user_name"`
	MembershipType string `json:"membership_type"`
}

type CreateResponse struct {
	ID             string `json:"id"`
	MembershipType string `json:"membership_type"`
}

dto.go 는 컨트롤러로부터 받은 정보를 정의하는 구조체이다. 우리는 Request 로는 Username, MembershipType 을 전달받고, Response 로는 ID와 MembershipType 을 내려주는 것으로 정의한다.

 

 

 

3. 테스트코드를 작성한다.

  • application_test.go
t.Run("멤버십을 생성한다.", func(t *testing.T) {
		app := NewApplication(*NewRepository(map[string]Membership{}))
		req := CreateRequest{"jenny", "naver"}
		res, err := app.Create(req)
		assert.Nil(t, err)
		assert.NotEmpty(t, res.ID)
		assert.Equal(t, req.MembershipType, res.MembershipType)
	})

구현하고자 하는 application.go 에서 구조체를 생성한다.

그리고 테스트하고자 하는 요청을 CreateRequest 로 정의하여 application.go 로 전달한다.

요청에 대한 CreateResponse 를 받아오고, 우리가 기대한 값이 맞는지 assert 를 통해 테스트를 진행한다.

 

 

 

 

4. 위의 테스트가 통과될 수 있도록 기능을 리팩토링한다.

  • application.go
func (app *Application) Create(request CreateRequest) (CreateResponse, error) {
	data := app.repository.data
	data[request.UserName] = Membership{request.UserName, request.UserName, request.MembershipType}
	return CreateResponse{request.UserName, request.MembershipType}, nil
}

 

 

 

5. 위의 과정을 반복하여 테스트 코드가 모두 정상적으로 실행될 수 있게 한다.

  • application.go
func (app *Application) Create(request CreateRequest) (CreateResponse, error) {
	if app.checkEmptyValue(request.UserName) {
		return CreateResponse{}, errEmptyUsername
	}

	if app.checkEmptyValue(request.MembershipType) {
		return CreateResponse{}, errEmptyMemberShip
	}

	if app.notMemberShipType(request.MembershipType) {
		return CreateResponse{}, errNotApplyMemberShip
	}

	exists := app.repository.checkExistId(request.UserName)
	if exists {
		return CreateResponse{}, errAlreadyExistUsername
	}

	data := app.repository.data

	data[request.UserName] = Membership{request.UserName, request.UserName, request.MembershipType}

	return CreateResponse{request.UserName, request.MembershipType}, nil
}

 

 

Reference


Comments