주의 : 이번 글은 필자가 테이블 주도 테스트라는 개념을 배우기 전에 작성된 글이다. 이 글의 내용이 정형화된 개념인 테이블 주도 테스트에 관해 알고 싶다면 해당 글을 참조하자.
들어가기에 앞서...
이전 글의 방식으로 테스트 케이스를 작성해도 TDD를 적용하는 데에는 크게 문제가 없다. 하지만 기능이 단순한 함수임에도 테스트 코드가 길고 반복되는 부분이 많다. 이번에는 이 테스트 함수를 다듬어보도록 하겠다.
1. 반복문
같은 함수를 반복적으로 호출하면 먼저 반복문을 생각해봐야 한다. 반복문을 작성하는 법은 간단하다. 반복되는 부분은 반복문에 그대로 넣고 반복되지 않는 부분은 변수로 집어넣으면 된다. 이전 글에서 짠 코드를 보자.
func TestAdd(t *testing.T) {
if Add(1, 3) != 4 {
t.Errorf("Errors in case 1 : Expected 4, Actual %d", Add(1, 3))
}
if Add(1, 0) != 1 {
t.Errorf("Errors in case 2 : Expected 1, Actual %d", Add(1, 0))
}
if Add (1, -1) != 0 {
t.Errorf("Errors in case 3 : Expected 0, Actual %d", Add(1, -1))
}
}
함수 TestAdd에서 반복되는 부분은 함수 Add 호출과 t.Errorf 호출 부분이고 반복되지 않는 부분은 각 case의 번호와 위 함수들의 매개변수이다. 이렇게 반복문을 만들 때 가장 간단한 방법은 반복되는 부분-즉, 반복시킬 부분-을 배열로 만드는 것이다. 이는데이터와 코드의 분리 라고도 불린다. 코드를 보면 감이 잡힐 것이다.
func TestAdd2(t *testing.T) {
set := [][]int{{1, 1, 3, 4}, {2, 1, 0, 1}, {3, 1, -1, 0}}
for _, v := range set {
if Add(v[1], v[2]) != v[3] {
t.Errorf("Errors in case %d : Expected %d, Actual %d", v[0], v[3], Add(v[1], v[2]))
}
}
}
그리고 case 번호는 배열의 인덱스 값으로 받아낼 수 있기 때문에 아래와 같이 수정할 수 있다.
func TestAdd2(t *testing.T) {
set := [][]int{{1, 3, 4}, {1, 0, 1}, {1, -1, 0}}
for i, v := range set {
if Add(v[0], v[1]) != v[2] {
t.Errorf("Errors in case %d : Expected %d, Actual %d", i+1, v[2], Add(v[0], v[1]))
}
}
}
이제 테스트 케이스를 추가할 때에는 배열 안에 테스트할 케이스를 추가만 하면 된다. 전보다는 테스트 케이스를 넣기 편해졌고 코드도 꽤 깔끔해졌다. 하지만 위의 함수에 테스트 케이스를 넣으려면 테스트 코드 전체를 보거나 주석을 달아 설명해야 한다. 이런 간단한 함수에서야 바로 추가할 수 있지만 함수가 복잡해질 수록 테스트 케이스를 추가할 때마다 골치가 아파진다.
2. 구조체화
위의 단점에 대한 해결책으로는 테스트용 구조체를 만드는 것을 들 수 있다. 사실 데이터들을 구조체(클래스)로 만드는 것은 객체 지향 프로그래밍에 있어서 아주 기본적인 발상이다.
type testObj struct {
arg1 int
arg2 int
result int
}
그 후에 이 구조체를 이용해 테스트 케이스를 작성하면 된다.
func TestAdd3(t *testing.T) {
set := []testObj{{1, 3, 4}, {1, 0, 1}, {1, -1, 0}}
for i, v := range set {
if Add(v.arg1, v.arg2) != v.result {
t.Errorf("Errors in case %d : Expected %d, Actual %d", i, v.result, Add(v.arg1, v.arg2))
}
}
}
코드 자체는 살짝 길어졌지만 가동성이 훨씬 좋아졌다. GoLand 같은 IDE를 이용하면 각각의 매개변수가 들어갈 속성 이름을 알려주기 때문에 더 가동성이 향상된다. 테스트 파일을 짜는 것은 새로운 프로그램을 만드는 것과 비슷하다고 볼 수 있다.(이는 TDD의 단점이기도 하다. 작동할 프로그램 하나를 위해 프로그램을 두개나 작성해야 한다니!)
3. 외부 패키지
위에서 언급했듯 테스트를 작성하는 것은 새로운 프로그램을 작성하는 것과 같다고 볼 수 있다. 그리고 프로그램을 작성할 때 외부 패키지(라이브러리)의 힘을 빌리는 것은 프로그래머에게 있어서 당연한 일이다. 즉, 테스트를 좀 더 편하게 해주는 패키지가 존재하지 않을 이유는 없다. 우선 명령행에서 외부 패키지를 받아보자.
go get github.com/stretchr/testify
그 후 test_Calc.go로 돌아와서 두 패키지를 import한다.
import "fmt"
import "github.com/stretchr/testify/assert"
이제 함수를 아래와 같이 작성하면 된다.
func TestAdd4(t *testing.T) {
set := []testObj{{1, 3, 4}, {1, 0, 1}, {1, -1, 0}}
for i, v := range set {
assert.Equal(t, v.result, Add(v.arg1, v.arg2), fmt.Sprintf("Errors in case %d", i))
}
}
위의 조건문 대신에 비교 함수를 넣어서 더 깔끔해졌다. 함수 asset.Equal이 두 값이 같은 지 테스트하는 데에 쓴다는 정도로만 설명하겠다. (특정 외부 패키지를 설명할 생각은 아니다.) 어떤 외부 패키지를 쓸지는 자신이 정하기 나름이므로 위의 패키지나 다른 패키지의 명세를 본 후에 그에 맞게 함수를 이용하면 된다.
마치며...
이번 글에서는 Go언어의 테스트 파일을 Simple & Clear하게 작성하기 위한 방법에 대해 알아보았다. 위의 기술을 이용하면 테스트 케이스를 추가하기 쉽고 테스트 파일의 유지보수가 편리해질 것이다.
go test는 Go언어에 기본적으로 내장되어 있는 프레임워크인 만큼 공식적이고 사용하기 쉬운 테스트 프레임워크이다. 이 글을 읽고 Go언어에 TDD를 적용하는게 어렵지 않다는 것을 깨닫는 것이 필자의 바램이다.