1. 단위 테스트 소개
단위 테스트는 프로그램에서 가장 작은 테스트 가능한 단위인 함수나 메서드 등을 확인하고 검증하는 것을 말합니다. 이것은 코드가 예상대로 작동하는지 확인하고, 개발자가 코드를 변경할 때 기존 기능을 실수로 망가뜨리지 않도록 합니다.
고 언어의 프로젝트에서는 단위 테스트의 중요성이 명백합니다. 먼저, 코드 품질을 향상시키고, 코드를 변경하는 데 더 많은 확신을 주어 개발자가 변경을 더 쉽게 할 수 있습니다. 둘째, 단위 테스트는 코드에 대한 문서로서 작용하여 예상 동작을 설명할 수 있습니다. 또한, 연속적인 통합 환경에서 자동으로 단위 테스트를 실행하면 새롭게 도입된 버그를 빨리 발견하여 소프트웨어의 안정성을 향상시킬 수 있습니다.
2. testing
패키지를 사용한 기본 테스트 수행
고 언어의 표준 라이브러리에는 테스트를 작성하고 실행하기 위한 도구와 기능을 제공하는 testing
패키지가 포함되어 있습니다.
2.1 첫 번째 테스트 케이스 작성
테스트 함수를 작성하려면 파일 이름 뒤에 _test.go
접미사가 있는 파일을 만들어야 합니다. 예를 들어, 소스 코드 파일의 이름이 calculator.go
라면 해당 테스트 파일의 이름은 calculator_test.go
여야 합니다.
그 다음, 테스트 함수를 작성할 차례입니다. 테스트 함수는 testing
패키지를 가져와야 하며 특정한 형식을 따라야 합니다. 다음은 간단한 예제입니다:
// calculator_test.go
package calculator
import (
"testing"
"fmt"
)
// 덧셈 함수 테스트
func TestAdd(t *testing.T) {
result := Add(1, 2)
expected := 3
if result != expected {
t.Errorf("기대값 %v인데, %v를 받았습니다", expected, result)
}
}
이 예제에서 TestAdd
는 가상의 Add
함수를 테스트하는 테스트 함수입니다. Add
함수의 결과가 기대 결과와 일치하면 테스트는 통과하고, 그렇지 않은 경우 t.Errorf
가 호출되어 테스트 실패에 대한 정보를 기록합니다.
2.2 테스트 함수의 명명 규칙 및 서명 이해
테스트 함수는 반드시 Test
로 시작해야 하며 소문자가 아닌 임의의 문자열이 뒤따라야 하며, 그들의 유일한 매개변수는 testing.T
의 포인터여야 합니다. 예제에서 볼 수 있듯이 TestAdd
는 올바른 명명 규칙과 서명을 따르고 있습니다.
2.3 테스트 케이스 실행
명령줄 도구를 사용하여 테스트 케이스를 실행할 수 있습니다. 특정 테스트 케이스를 위해 다음 명령을 실행합니다:
go test -v // 현재 디렉토리의 테스트를 실행하고 자세한 출력을 표시합니다
특정 테스트 케이스를 실행하려면 -run
플래그 뒤에 정규 표현식을 사용할 수 있습니다:
go test -v -run TestAdd // TestAdd 테스트 함수만 실행
go test
명령은 자동으로 모든 _test.go
파일을 찾아 기준을 충족하는 모든 테스트 함수를 실행합니다. 모든 테스트가 통과하면 명령줄에 PASS
와 유사한 메시지가 표시되며, 테스트가 실패하면 해당 오류 메시지와 함께 FAIL
이 표시됩니다.
3. 테스트 케이스 작성
3.1 t.Errorf
및 t.Fatalf
를 사용한 오류 보고
고 언어에서는 테스트 프레임워크가 오류를 보고하는 데 사용하는 다양한 메서드를 제공합니다. 가장 일반적으로 사용되는 두 가지 함수는 Errorf
와 Fatalf
로, 둘 다 testing.T
객체의 메서드입니다. Errorf
는 테스트에서 오류를 보고하지만 현재 테스트 케이스를 중지하지 않으며, Fatalf
는 오류를 보고한 후 현재 테스트를 즉시 중지합니다. 테스트 요구에 따라 적절한 방법을 선택하는 것이 중요합니다.
Errorf
사용 예제:
func TestAdd(t *testing.T) {
got := Add(1, 2)
want := 3
if got != want {
t.Errorf("Add(1, 2) = %d; 원했던 값 %d", got, want)
}
}
오류가 검출되면 즉시 테스트를 중지하려면 Fatalf
를 사용할 수 있습니다:
func TestSubtract(t *testing.T) {
got := Subtract(5, 3)
if got != 2 {
t.Fatalf("Subtract(5, 3) = %d; 원했던 값 2", got)
}
}
일반적으로 오류로 인해 후속 코드가 올바르게 실행되지 않거나 테스트 실패가 미리 확인될 경우에는 Fatalf
를 사용하는 것이 좋습니다. 그렇지 않으면 보다 포괄적인 테스트 결과를 얻기 위해 Errorf
를 사용하는 것이 좋습니다.
3.2 서브테스트 구성 및 실행
Go 언어에서는 t.Run
을 사용하여 서브테스트를 구성할 수 있으며, 이를 통해 테스트 코드를 보다 체계적으로 작성할 수 있습니다. 서브테스트에는 각각의 Setup
과 Teardown
을 가질 수 있고, 개별적으로 실행하여 큰 유연성을 제공합니다. 이는 복잡한 테스트나 매개변수화된 테스트를 수행할 때 특히 유용합니다.
서브테스트 t.Run
사용 예시:
func TestMultiply(t *testing.T) {
testcases := []struct {
name string
a, b, expected int
}{
{"2x3", 2, 3, 6},
{"-1x-1", -1, -1, 1},
{"0x4", 0, 4, 0},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
if got := Multiply(tc.a, tc.b); got != tc.expected {
t.Errorf("Multiply(%d, %d) = %d; want %d", tc.a, tc.b, got, tc.expected)
}
})
}
}
만약 "2x3"이라는 서브테스트만 개별적으로 실행하고 싶다면, 다음과 같은 명령을 명령줄에서 실행할 수 있습니다:
go test -run TestMultiply/2x3
서브테스트 이름은 대소문자를 구분해야 합니다.
4. 테스트 전후 준비 작업
4.1 준비 및 정리
테스트를 수행할 때 종종 테스트를 위한 초기 상태를 준비해야 하며(예: 데이터베이스 연결, 파일 생성 등), 마찬가지로 테스트가 완료된 후 정리 작업을 해주어야 합니다. Go 언어에서는 일반적으로 테스트 함수 내에서 직접 Setup
과 Teardown
을 수행하고, t.Cleanup
함수를 통해 정리 작업의 콜백 함수를 등록할 수 있습니다.
간단한 예시는 다음과 같습니다:
func TestDatabase(t *testing.T) {
db, err := SetupDatabase()
if err != nil {
t.Fatalf("setup failed: %v", err)
}
// Register a cleanup callback to ensure the database connection is closed when the test is finished
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Errorf("failed to close database: %v", err)
}
})
// 테스트 수행...
}
TestDatabase
함수에서는 먼저 SetupDatabase
함수를 호출하여 테스트 환경을 설정합니다. 그런 다음, t.Cleanup()
를 사용하여 테스트가 완료된 후 정리 작업을 수행할 함수를 등록합니다. 위 예시에서는 데이터베이스 연결을 닫는 작업이죠. 이렇게 함으로써 테스트가 성공해도 실패해도 자원이 올바르게 해제되도록 할 수 있습니다.
5. 테스트 효율성 향상
테스트 효율성을 향상시키면 개발을 보다 신속히 반복하고 문제를 신속히 발견하며 코드 품질을 보장할 수 있습니다. 아래에서는 테스트 커버리지, 테이블 기반 테스트 및 목(mock) 사용에 대해 논의하겠습니다.
5.1 테스트 커버리지와 관련 도구
go test
도구는 매우 유용한 테스트 커버리지 기능을 제공하여 코드의 어떤 부분이 테스트 케이스에 의해 실행되었는지를 이해하고, 테스트 케이스에 의해 커버되지 않은 부분을 발견할 수 있게 도와줍니다.
go test -cover
명령을 사용하면 현재의 테스트 커버리지 백분율을 확인할 수 있습니다:
go test -cover
더 자세히 어떤 코드 라인이 실행되었고 어떤 것이 실행되지 않았는지 이해하고 싶다면, 커버리지 데이터 파일을 생성하는 -coverprofile
매개변수를 사용할 수 있습니다. 그런 다음 go tool cover
명령을 사용하여 자세한 테스트 커버리지 보고서를 생성할 수 있습니다.
go test -coverprofile=coverage.out
go tool cover -html=coverage.out
위 명령은 웹 보고서를 열어 시각적으로 테스트가 적용된 코드 라인과 그렇지 않은 코드 라인을 보여줍니다. 초록색은 테스트가 적용된 코드를, 빨간색은 테스트가 되지 않은 코드를 나타냅니다.
5.2 Mock 사용하기
테스트에서 종종 외부 종속성을 시뮬레이션해야 하는 시나리오에 직면합니다. Mock은 이러한 종속성을 시뮬레이션하여 테스트 환경에서 특정 외부 서비스나 리소스에 의존하지 않도록 도와줍니다.
Go 커뮤니티에는 testify/mock
과 gomock
과 같은 많은 mock 도구가 있습니다. 이러한 도구들은 일반적으로 mock 객체를 생성하고 사용하기 위한 일련의 API를 제공합니다.
여기 testify/mock
을 사용하는 기본적인 예제가 있습니다. 먼저, 인터페이스와 해당 mock 버전을 정의해야 합니다:
type DataService interface {
FetchData() (int, error)
}
type MockDataService struct {
mock.Mock
}
func (m *MockDataService) FetchData() (int, error) {
args := m.Called()
return args.Int(0), args.Error(1)
}
테스트에서는 MockDataService
를 실제 데이터 서비스로 대체하는 데 사용할 수 있습니다:
func TestSomething(t *testing.T) {
mockDataSvc := new(MockDataService)
mockDataSvc.On("FetchData").Return(42, nil) // 예상 동작 설정
result, err := mockDataSvc.FetchData() // mock 객체 사용
assert.NoError(t, err)
assert.Equal(t, 42, result)
mockDataSvc.AssertExpectations(t) // 예상 동작이 발생했는지 확인
}
위의 방법을 통해 테스트에서 외부 서비스나 데이터베이스 호출 등에 의존하지 않거나 이를 격리할 수 있습니다. 이렇게 하면 테스트 실행 속도가 빨라지고 테스트가 더 안정적이고 신뢰할 수 있게 됩니다.
6. 고급 테스팅 기법
Go 단위 테스트의 기본을 마스터한 후 더 고급 테스팅 기법을 더 알아보면 더 견고한 소프트웨어를 개발하고 테스트 효율성을 향상시킬 수 있습니다.
6.1 비공개 함수 테스트
Golang에서 비공개 함수는 일반적으로 내보내지지 않은 함수, 즉 소문자로 시작하는 함수를 가리킵니다. 보통 코드의 사용성을 반영하는 공개 인터페이스를 테스트하는 것을 선호하지만, 비공개 함수를 직접 테스트해야 하는 경우도 있습니다. 특히 비공개 함수가 복잡한 로직을 가지고 여러 공개 함수에서 호출될 때입니다.
비공개 함수 테스트는 패키지 외부에서 액세스할 수 없기 때문에 공개 함수를 테스트하는 것과 다릅니다. 흔한 기술은 동일한 패키지에 테스트 코드를 작성하여 비공개 함수에 액세스할 수 있게 하는 것입니다.
다음은 간단한 예제입니다:
// calculator.go
package calculator
func add(a, b int) int {
return a + b
}
해당하는 테스트 파일은 다음과 같습니다:
// calculator_test.go
package calculator
import "testing"
func TestAdd(t *testing.T) {
expected := 4
actual := add(2, 2)
if actual != expected {
t.Errorf("expected %d, got %d", expected, actual)
}
}
테스트 파일을 동일한 패키지에 놓음으로써 add
함수를 직접 테스트할 수 있습니다.
6.2 일반적인 테스트 패턴과 모범 사례
Golang의 단위 테스트에는 테스트 작업을 용이하게 하는 몇 가지 일반적인 패턴이 있습니다. 이러한 패턴은 코드 가독성과 유지 관리성을 유지하는 데 도움이 됩니다.
-
테이블 기반 테스트
테이블 기반 테스트는 테스트 입력 및 예상 출력을 구성하는 방법입니다. 테스트 케이스 집합을 정의한 다음 해당 테스트 케이스를 루프하여 테스트하는 방식으로, 이 방법은 쉽게 새로운 테스트 케이스를 추가할 수 있게 하며 코드를 읽고 유지보수하는 데 도움이 됩니다.
// calculator_test.go
package calculator
import "testing"
func TestAddTableDriven(t *testing.T) {
var tests = []struct {
a, b int
want int
}{
{1, 2, 3},
{2, 2, 4},
{5, -1, 4},
}
for _, tt := range tests {
testname := fmt.Sprintf("%d,%d", tt.a, tt.b)
t.Run(testname, func(t *testing.T) {
ans := add(tt.a, tt.b)
if tt.want != ans {
t.Errorf("got %d, want %d", ans, tt.want)
}
})
}
}
-
테스트용 Mock 사용
Mocking은 기능의 여러 부분을 테스트하기 위해 종속성을 대체하는 테스트 기법입니다. Golang에서는 인터페이스가 모의를 구현하는 주요 방법입니다. 인터페이스를 사용하여 모의 구현을 생성한 다음 테스트에서 사용할 수 있습니다.