Go 테스트 코드 작성법 – 업무에 사용하는 Go 언어 응용편 10

Go 테스트 코드
Go 테스트 코드

Go 언어를 배우면서 문법을 익혔다면, 이제 실제 개발에서 빠질 수 없는 테스트 코드 작성을 배워야 합니다. go 테스트 코드는 코드의 안정성과 신뢰성을 보장하는 핵심적인 요소입니다. 필자는 go언어를 활용하여 웹 사이트를 구성한 경험이 있으며, 그 과정에서 테스트 코드의 중요성을 몸소 느낄 수 있었습니다.

테스트 코드 없이 개발을 진행하면 나중에 코드를 수정할 때마다 전체 애플리케이션을 수동으로 확인해야 하는 번거로움이 생깁니다. 하지만 적절한 테스트가 있다면 코드 변경 후 몇 초 만에 모든 기능이 정상 작동하는지 확인할 수 있습니다.

Go의 기본 testing 패키지 이해하기

Go는 표준 라이브러리에 testing 패키지를 제공하여 별도의 외부 라이브러리 없이도 강력한 테스트를 작성할 수 있습니다. 이 패키지는 단위 테스트, 벤치마크 테스트, 예제 테스트를 모두 지원합니다.

테스트 파일은 반드시 _test.go로 끝나는 파일명을 가져야 하며, 테스트 함수는 Test로 시작하고 *testing.T 타입의 매개변수를 받아야 합니다. 이러한 규칙을 지키면 go test 명령어로 자동으로 테스트를 실행할 수 있습니다.

첫 번째 go 테스트 코드 작성하기

간단한 계산기 함수를 만들고 테스트해보겠습니다. 먼저 calculator.go 파일을 생성합니다:

Go
package main

func Add(a, b int) int {
    return a + b
}

func Multiply(a, b int) int {
    return a * b
}

func Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil
}

이 코드에서 Add 함수는 두 정수를 더하는 단순한 기능을, Multiply 함수는 곱셈을, Divide 함수는 나눗셈을 수행합니다. Divide 함수는 0으로 나누는 경우를 처리하기 위해 에러를 반환하도록 설계했습니다.

이제 calculator_test.go 파일을 생성하여 테스트를 작성해보겠습니다:

Go
package main

import (
    "testing"
)

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    expected := 5
    
    if result != expected {
        t.Errorf("Add(2, 3) = %d; expected %d", result, expected)
    }
}

func TestMultiply(t *testing.T) {
    result := Multiply(4, 5)
    expected := 20
    
    if result != expected {
        t.Errorf("Multiply(4, 5) = %d; expected %d", result, expected)
    }
}

위 테스트 코드에서 TestAdd 함수는 Add 함수가 올바르게 작동하는지 검증합니다. t.Errorf를 사용하여 예상값과 실제값이 다를 때 오류 메시지를 출력하도록 했습니다. 이는 테스트가 실패했을 때 어떤 부분에서 문제가 발생했는지 명확하게 파악할 수 있게 해줍니다.

테이블 드리븐 테스트로 go언어 테스트 코드 고도화하기

여러 입력값에 대해 테스트하려면 테이블 드리븐 테스트 방식을 사용하는 것이 효율적입니다:

Go
func TestAddTableDriven(t *testing.T) {
    testCases := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive numbers", 2, 3, 5},
        {"negative numbers", -1, -2, -3},
        {"zero and positive", 0, 5, 5},
        {"positive and zero", 7, 0, 7},
    }
    
    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            result := Add(tc.a, tc.b)
            if result != tc.expected {
                t.Errorf("Add(%d, %d) = %d; expected %d", tc.a, tc.b, result, tc.expected)
            }
        })
    }
}

이 코드는 구조체 슬라이스를 사용하여 다양한 테스트 케이스를 정의합니다. 각 테스트 케이스는 name 필드로 구분되며, t.Run을 사용하여 개별적으로 실행됩니다. 이 방식의 장점은 새로운 테스트 케이스를 추가할 때 슬라이스에만 항목을 추가하면 된다는 점입니다. 필자는 추후에 gin 웹 프레임워크를 통해 고도화된 사이트를 만들었을 때 이러한 테이블 드리븐 테스트 방식을 적극 활용했습니다.

에러 처리를 포함한 go언어 테스트 코드 작성

실제 애플리케이션에서는 에러 처리가 중요합니다. Divide 함수의 테스트를 작성해보겠습니다:

Go
func TestDivide(t *testing.T) {
    // 정상적인 나눗셈 테스트
    result, err := Divide(10.0, 2.0)
    if err != nil {
        t.Errorf("Unexpected error: %v", err)
    }
    expected := 5.0
    if result != expected {
        t.Errorf("Divide(10.0, 2.0) = %f; expected %f", result, expected)
    }
    
    // 0으로 나누기 에러 테스트
    _, err = Divide(10.0, 0.0)
    if err == nil {
        t.Error("Expected error for division by zero, but got none")
    }
}

이 테스트는 두 가지 시나리오를 검증합니다. 첫 번째는 정상적인 나눗셈이 올바른 결과를 반환하고 에러가 발생하지 않는지 확인하고, 두 번째는 0으로 나눌 때 적절한 에러가 발생하는지 검증합니다. 에러 처리 테스트에서는 에러가 예상대로 발생하지 않을 때도 테스트 실패로 처리해야 합니다.

테스트 실행과 결과 해석

작성한 go 테스트 코드를 실행하려면 터미널에서 다음 명령어를 사용합니다:

go test

모든 테스트가 통과하면 PASS라는 메시지와 함께 실행 시간이 표시됩니다. 특정 테스트만 실행하고 싶다면:

go test -run TestAdd

더 자세한 출력을 원한다면 -v 옵션을 사용합니다:

go test -v

이렇게 하면 각 테스트 함수의 실행 결과와 소요 시간을 개별적으로 확인할 수 있습니다.

실제 개발에서의 활용

필자의 경험상 테스트 코드는 리팩토링할 때 특히 유용했습니다. 코드 구조를 개선하거나 성능을 최적화할 때 기존 기능이 여전히 정상적으로 작동하는지 빠르게 확인할 수 있기 때문입니다. 또한 팀 개발 환경에서는 다른 개발자가 작성한 코드를 수정할 때 테스트를 통해 의도치 않은 부작용을 방지할 수 있습니다.

Go의 testing 패키지는 단순하면서도 강력한 기능을 제공합니다. 복잡한 설정 없이도 효과적인 테스트를 작성할 수 있어 개발 생산성을 크게 향상시킬 수 있습니다. 지금부터라도 새로운 함수를 작성할 때마다 테스트 코드를 함께 작성하는 습관을 기르시기 바랍니다.