새로운 언어를 공부할땐 책을 보면서 syntax를 익히거나 간단한 프로젝트를 진행했었다.
이번엔 Go를 공부하게됬는데 TDD를 통해서 언어에 익숙해지는 방법을 시도해봤다. 이런 방법은 언어에 익숙해지면서 Test, Refactoring에도 자연스럽게 익숙해질 수 있는 장점이 있다. 이 글에선 TDD를 통해 간단하게 Go를 시작할 수 있는 방법에 대해서 정리해보려 한다.
Go 개발환경 셋팅하기
Go를 설치하는 방법은 두 가지가 있다.
- Go Download 에서 다운 받아서 설치
- GVM을 이용해서 설치
나는 버전을 유연하게 관리할 수 있는 GVM 방식을 선호한다.
GVM을 통한 자세한 설치방법은 아래 링크를 참고하도록 하자.
https://select995.netlify.com/go/module/gvm
Go 프로젝트 생성
이전에 Go를 잠깐 배웠을땐 GOPATH
를 워크스페이스 처럼 사용해야 해서 불편하게 개발을 했던 기억이 있다. Go Modules를 사용하면 GOPATH
바깥에서도 Go 프로젝트를 만들고 개발할 수 있다.
go mod 명령을 통해 프로젝트를 생성한다.
1 | go mod init <Repo/프로젝트 명> |
Hello, World TDD
아래 Repo에서는 Test를 작성하면서 Go에 대해서 알아갈 수 있도록 여러가지 토픽에 튜토리얼을 제공한다.
https://github.com/quii/learn-go-with-tests
가장 첫 번째 토픽인 Hello, World를 시작해보자.
단순히 Hello, World를 찍는 프로그램을 TDD way로 진행해 볼 수 있다.
먼저 위에서 생성한 프로젝트에 hello.go 를 만들어보자.
1 | package main |
go 에서 entrypoint는 main package 의 main 함수이다.
이렇게 작성하고 go run
을 하면 Hello, world 문구가 출력된다.
Hello라는 함수를 분리한 이유는 Test를 돌리기위해서이다.fmt.Println
은 stdout에 출력하는 side-effect를 가지고 있기 떄문에 Test하기 어려운 함수가된다.
이제 Test code를 작성해보자.
내가 해본 다른언어들은 Test를 돌리기위해서 다른 라이브러리나 프레임워크를 필요로 했다.
(Java - Junit
, Javascript - Jest
, Mocha
)
Go에서는 Test를 위해서 Test Runner를 추가할 필요가 없다.
단지 파일명을 xxx_test.go
형식으로 만들어주면 된다. 이 예제에서는 hello.go
로 작성했기때문에 hello_test.go
로 만들어주면 된다.
hello_test.go
1
2
3
4
5
6
7
8
9
10
11
12package main
import "testing"
func TestHello(t *testing.T) {
got := Hello()
want := "Hello, world"
if got != want {
t.Errorf("got %q want %q", got, want)
}
}
여기서 got은 Test할 함수가 반환한 결과값이고 want는 Test에서 기대하는 값(expect)라고 볼 수 있다.
내장 모듈인 testing에는 Test를 위한 여러 유틸리티 함수들이 있다.
(Error, Log, Run…)
위 Test Code는 기대값과 실제 결과가 다르면 error를 내는 코드이다.
Assertion을 사용하려면 다른 라이브러리를 추가해야한다.
go test
명령을 통해서 작성해본 test가 정상적으로 동작하는지 알 수 있다.
일반적으로 개발할때는 실제 코드를 먼저 작성하고 정상적으로 동작하는지 실행을 해보거나 디버그로 버그를 찾는 식이다. TDD로 개발을 한다면 실제 코드보다 먼저 테스트를 작성하도록 권장한다.
전체적인 프로세스를 정리해보면 이런식일 것이다.
- 실패하는 Test 작성
- Test를 Pass할 수 있는 최소한의 코드를 작성
- 리팩토링
(RED -> GREEN -> REFACTOR)
만약 TDD 프로세스를 잘 모르겠다면 이 부분에 대해서 먼저 공부해는 것도 좋을 것 같다.
요구사항이 추가되었다. 단순히 Hello, world를 찍지 않고 인자로 이름을 받아서 처리하도록 변경해보자. 위 TDD 프로세스대로 먼저 테스트를 변경한다.
1 | got := Hello("Chris") |
이와 같이 먼저 어떤식으로 Hello라는 함수를 사용하고 어떤 결과를 기대하는지 작성한다.
IDE를 사용한다면 바로 lint error가 뜨는 것을 볼 수 있을것이다. (RED)
실제코드를 간단히 변경해서 Test를 Pass하도록 변경할 수 있다. (GREEN)1
2
3func Hello(name string) string {
return "Hello, " + name
}
Test를 돌려보자. OK 문구가 뜨는걸 볼수 있을것이다.
(만약 FAIL이 난다면 타이핑 실수를 하지 않았는지 확인해보자)
이제 리팩토링할 단계이다. 사실 이 코드에서는 리팩토링할 부분이 많지않다.
단순히 “Hello, “ 부분을 상수로 분리한다.
1 | const englishHelloPrefix = "Hello, " |
다시 Test를 돌려보자. (go test)
Success가 나오는 걸 확인할 수 있다. 만약 어떤 실수를 했다면 FAIL과 함께 Test code가 피드백을 줄 것이다.
다음 요구사항은 name 인자에 empty가 넘어올경우 기본값을 world로 설정해주는것이다.
테스트 해야될 사항이 두가지로 늘어났다. empty string(“”) 이 넘어올경우 Hello, world를 출력하는지 그리고 empty가 아닌 string이 넘어올 경우
하나의 Test case에서는 한 가지에 대해서만 명확히 Test를 작성하는 것이 좋다.
두가지 Test case로 분리해보자.
1 | func TestHello(t *testing.T) { |
TestHello
라는 함수는 Test Suite로 t.Run
은 하나의 Test Case로 볼 수 있을 것 같다.
아직 empty string에 대해 처리하지 않았기 때문에 에러가 날 것이다. 위에서 진행헀던 프로세스그대로 Test를 Pass할 수 있는 코드를 작성하고 리팩토링을 하면 된다.
Test를 Pass 시키기 전에 Test Code를 리팩토링 해보자. 지금은 got, want의 값이 다를 경우 error를 내는 부분이 중복되어있고 정확히 어떤 역할을 하는지 한 눈에 파악하기 어렵다.
1 | assertCorrectMessage := func(t *testing.T, got, want string) { |
assertCorrectMessage 라는 함수로 assert부분을 분리한다. 중복이 없어지고 가독성도 훨씬 좋아질 것이다. t.Helper()
함수는 error가 난 라인을 추적할 수 있도록 도와준다. 사용하지 않아도 라인을 알려주지만 정확히 에러가 난 라인이 아닌 assertCorrectMessage 함수가 있는 라인넘버를 알려줄 것이다.
Test를 Pass하도록 코드를 작성한다.1
2
3
4
5
6
7const englishHelloPrefix = "Hello, "
func Hello(name string) string {
if name == "" {
name = "world"
}
return englishHelloPrefix + name
}
다음 단계는 리팩토링이지만 여기선 그럴부분이 딱히 없어서 그대로 두면 될 것 같다.
위에서 소개한 Repo의 원문글을 보면 뒤에 2가지 단계 정도를 더 진행한다.
끝까지 진행해보면 TDD cycle을 익히는데 도움이 될 것 같다.
Tip
go test -v
- go test는 실패하면 실패한 Test Case를 알려주지만 성공시에는 성공했다는 OK 문구 하나만 나온다. test case를 같이 출력하고 싶다면 -v 옵션을 추가해보자
-
- 위 라이브러리를 사용하면 assert관련 여러 함수를 사용할 수 있다. 이런 함수들은 Test 실패시 실제 결과값과 어떤식으로 다른지 diff checking과 같은 부분도 제공을 하기 때문에 편리하다.
마치며
위 Hello, World는 정말 간단한 예제이지만 TDD 개발의 cycle에 익숙해질 수 있고 복잡하지 않은 코드를 연습하면서 언어를 익히는데도 도움이 많이 되었던 것 같다. go 를 새로 공부해야 한다면 TDD를 통해서 배우는 것도 괜찮은 방법일 것 같다.