Go testing 패키지

1 개요[ | ]

Go testing 패키지
  • Go 표준 라이브러리의 하나
  • 자동 테스트를 지원하는 Go 패키지
  • 모든 기능 실행을 자동화하는 "go test" 명령과 함께 사용하기 위한 것이다.
func TestXxx(*testing.T)
  • 여기서 Xxx는 소문자로 시작하지 않는다. 함수 이름은 테스트 루틴을 식별하는 역할을 한다.
  • 이러한 함수 내에서 Error, Fail 또는 관련 메서드를 사용하여 오류를 알린다.
  • 새 테스트 도구 모음을 작성하려면 여기에 설명된 대로 TestXxx 함수가 포함된 파일을 만들고 해당 파일에 "_test.go"로 끝나는 이름을 지정한다. 일반 패키지 빌드에서는 제외되지만 "go test" 명령어가 실행될 때는 포함된다.
  • 테스트 파일은 테스트 중인 패키지와 같은 패키지 또는 접미어 "_test"가 붙은 패키지에 있을 수 있다.
  • 테스트 파일이 같은 패키지에 있는 경우 다음 예제와 같이, 패키지 내의 unexported 식별자를 참조할 수 있다.
package abs

import "testing"

func TestAbs(t *testing.T) {
    got := Abs(-1)
    if got != 1 {
        t.Errorf("Abs(-1) = %d; want 1", got)
    }
}
  • 파일이 "_test" 패키지에 있는 경우 테스트 중인 패키지를 명시적으로 가져와야 하며 exported 식별자만 사용할 수 있다. 이것은 "블랙박스" 테스트로 알려져 있다.
package abs_test

import (
	"testing"

	"path_to_pkg/abs"
)

func TestAbs(t *testing.T) {
    got := abs.Abs(-1)
    if got != 1 {
        t.Errorf("Abs(-1) = %d; want 1", got)
    }
}

2 Benchmark[ | ]

함수 형식

func BenchmarkXxx(*testing.B)

위와 같은 형식은 벤치마크로 간주되며 -bench 플래그가 입력되면 "go test" 명령어에 의해 실행된다. 벤치마크는 순차적으로 실행된다.

테스트 플래그에 대한 설명은 https://golang.org/cmd/go/#hdr-Testing_flags 을 참조하자.

아래는 벤치마크 함수 예시이다.

func BenchmarkRandInt(b *testing.B) {
    for i := 0; i < b.N; i++ {
        rand.Int()
    }
}

벤치마크 함수는 대상 코드를 b.N 번 실행된다. 벤치마크 실행 중에 b.N은, 시간을 안정적으로 측정할 수 있을 만큼 벤치마크 함수가 충분히 오래 지속되도록 조정된다.

BenchmarkRandInt-8   	68453040	        17.8 ns/op

위 결과는 루프당 17.8ns의 속도로 68453040회 실행되었음을 의미한다.

벤치마크를 실행하기 전에 비용이 많이 드는 설정이 필요한 경우에는 타이머를 리셋할 수 있다.

func BenchmarkBigLen(b *testing.B) {
    big := NewBig()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        big.Len()
    }
}

벤치마크가 병렬 세팅에서 성능을 테스트해야 하는 경우 RunParallel 헬퍼 함수를 사용할 수 있다. 이러한 벤치마크는 go test -cpu 플래그와 함께 사용하기 위한 것이다.

func BenchmarkTemplateParallel(b *testing.B) {
    templ := template.Must(template.New("test").Parse("Hello, {{.}}!"))
    b.RunParallel(func(pb *testing.PB) {
        var buf bytes.Buffer
        for pb.Next() {
            buf.Reset()
            templ.Execute(&buf, "World")
        }
    })
}

벤치마크 결과 형식에 대한 자세한 사양은 https://golang.org/design/14313-benchmark-format 에 나와 있다 .

https://golang.org/x/perf/cmd 에는 벤치마크 결과 처리를 위한 표준 도구가 있다 . 특히 https://golang.org/x/perf/cmd/benchstat 는 통계적으로 강건(robust)한 A/B 비교를 수행한다.

3 Example[ | ]

이 패키지는 또한 Example 코드를 실행하고 검증한다. 예시 함수에는 "Output:"으로 시작하고 테스트가 실행될 때 함수의 표준 출력과 비교되는 결론 라인 주석이 포함될 수 있다. (비교시에 앞뒤의 공백은 무시한다.) 다음은 Example의 예시이다.

func ExampleHello() {
    fmt.Println("hello")
    // Output: hello
}

func ExampleSalutations() {
    fmt.Println("hello, and")
    fmt.Println("goodbye")
    // Output:
    // hello, and
    // goodbye
}

주석 접두어 "Unordered output:"은 "Output:"과 유사하지만 아무 줄이나 매치된다.

func ExamplePerm() {
    for _, value := range Perm(5) {
        fmt.Println(value)
    }
    // Unordered output: 4
    // 2
    // 1
    // 3
    // 0
}

출력 주석이 없는 Example 함수는 컴파일되지만 실행되지는 않는다.

패키지, 함수 F, 타입 T, 타입 T의 메소드 M에 대한 Example을 선언하는 명명 규칙은 다음과 같다.

func Example() { ... }
func ExampleF() { ... }
func ExampleT() { ... }
func ExampleT_M() { ... }

패키지/유형/함수/메서드에 대한 여러 Example 함수에는 그 이름에 별도의 접미사를 붙일 수 있다. 접미사는 소문자로 시작해야 한다.

func Example_suffix() { ... }
func ExampleF_suffix() { ... }
func ExampleT_suffix() { ... }
func ExampleT_M_suffix() { ... }

테스트 파일 내에 Example 함수 1개와 1개 이상의 다른 함수/타입/변수/상수 선언이 있으면서 테스트/벤치마크 함수는 없다면 Example로서 표시된다.

4 Fuzz[ | ]

형식

func FuzzXxx(*testing.F)

위와 같은 형식은 fuzz 테스트로 간주된다.

예시

func FuzzHex(f *testing.F) {
  for _, seed := range [][]byte{{}, {0}, {9}, {0xa}, {0xf}, {1, 2, 3, 4}} {
    f.Add(seed)
  }
  f.Fuzz(func(t *testing.T, in []byte) {
    enc := hex.EncodeToString(in)
    out, err := hex.DecodeString(enc)
    if err != nil {
      t.Fatalf("%v: decode: %v", in, err)
    }
    if !bytes.Equal(in, out) {
      t.Fatalf("%v: not equal after round trip: %v", in, out)
    }
  })
}

fuzz 테스트는 시드 코퍼스 또는 기본으로 실행되는 입력 세트를 유지관리하며, 입력 생성을 시드할 수 있다. 시드 입력은 (*F).Add를 호출하거나 fuzz 테스트가 포함된 패키지 내의 testdata/fuzz/<Name> (여기서 <Name>은 fuzz 테스트의 이름) 디렉토리에 파일을 저장하여 등록할 수 있다. 시드 입력은 선택사항이지만, 코드 커버리지가 좋은 작은 시드 입력 세트가 제공되면 퍼징 엔진이 버그를 더 효율적으로 찾을 수 있다. 이러한 시드 입력은 퍼징을 통해 확인된 버그에 대한 회귀 테스트가 될 수 있다.

fuzz 테스트 내에서 (*F).Fuzz에 전달된 함수는 fuzz 대상으로 간주된다. fuzz 대상은 랜덤 입력에 대한 1개 이상의 매개변수가 붙는 *T 매개변수를 받아야 한다. (*F).Add에 전달되는 인수의 타입은 이러한 매개변수의 타입과 동일해야 한다. fuzz 대상은 테스트와 동일한 방식으로 문제를 발견했다는 신호를 보낼 수 있다. 즉, T.Fail(또는 T.Error 또는 T.Fatal과 같이 그것을 호출하는 모든 메서드)을 호출하거나 패닉 상태에 빠짐으로서 가능하다.

fuzzing이 활성화되면(-fuzz 플래그를 특정 fuzz 테스트와 일치하는 정규식으로 설정하여) 시드 입력을 반복적으로 랜덤으로 변경하여 생성된 인수를 가지고 fuzz 대상을 호출한다. 지원되는 플랫폼에서, 'go test'는 퍼징 커버리지 장치를 가지고 테스트 실행파일을 컴파일한다. 퍼징 엔진은 해당 장치를 사용하여 커버리지를 확장하는 입력을 찾고 캐시하여 버그를 찾을 가능성을 높인다. 주어진 입력에 대해 fuzz 대상이 실패하면 퍼징 엔진은 패키지 디렉토리 내의 testdata/fuzz/<Name> 디렉토리에 있는 파일에 실패를 유발한 입력을 기록한다. 이 파일은 나중에 시드 입력으로 활용된다. 해당 위치에 파일을 쓸 수 없는 경우(예: 디렉터리가 읽기 전용인 경우), fuzzing 엔진은 그 대신에 빌드 캐시 내의 fuzz 캐시 디렉토리에 파일을 기록한다.

퍼징이 비활성화되면, F.Add에 등록된 시드 입력과 testdata/fuzz/<Name>의 시드 입력으로 fuzz 대상이 호출된다. 이 모드에서, fuzz 테스트는 서브테스트가 T.Run 대신 F.Fuzz로 시작되는 일반적인 테스트와 매우 유사하게 작동한다.

퍼징에 대한 문서는 https://go.dev/doc/fuzz 를 참조하자.

5 Skipping[ | ]

테스트 또는 벤치마크는 *T 또는 *B의 Skip 메서드를 호출하여 런타임에서 skip할 수 있다.

func TestTimeConsuming(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping test in short mode.")
    }
    ...
}
  • *T의 Skip 메서드는 입력이 유효하지 않은 경우 fuzz 대상에서 사용할 수 있지만, 실패하는 입력으로 간주해서는 안 된다. 아래는 그 예시이다.
func FuzzJSONMarshaling(f *testing.F) {
    f.Fuzz(func(t *testing.T, b []byte) {
        var v interface{}
        if err := json.Unmarshal(b, &v); err != nil {
            t.Skip()
        }
        if _, err := json.Marshal(v); err != nil {
            t.Errorf("Marshal: %v", err)
        }
    })
}

6 서브테스트/서브벤치마크[ | ]

T와 B의 Run 메서드를 사용하면, 각각에 대해 개별적으로 함수를 정의할 필요 없이 서브테스트와 서브벤치마크를 정의할 수 있다. 이를 통해 테이블 ​​기반 벤치마크 및 계층적 테스트 생성으로 활용이 가능하다. 또한 공통 셋업과 뒷정리 코드를 공유하는 방법을 제공한다.

func TestFoo(t *testing.T) {
    // <setup code>
    t.Run("A=1", func(t *testing.T) { ... })
    t.Run("A=2", func(t *testing.T) { ... })
    t.Run("B=1", func(t *testing.T) { ... })
    // <tear-down code>
}

각 서브테스트 및 서브벤치마크에는 고유한 이름이 있다. 이는 최상위 테스트 이름과, 슬래시로 구분되는 Run에 전달된 이름, 그리고 선택적으로는 따라붙는 시퀀 번호의 조합이다.

-run, -bench, -fuzz 명령줄 플래그에 대한 인수는 테스트 이름과 매칭되는 정규표현식이다. 서브테스트 같이 슬래시로 구분된 여러 요소가 있는 테스트의 경우, 인수 자체가 슬래시로 구분되며 표현식은 각 이름 요소와 차례로 매칭된다. 빈 표현식은 모든 문자열과 매칭된다. 예를 들어 다음과 같이 "어느 이름이 포함하고 있는가"를 의미하는 "매칭"이다.

go test -run ''        # 모든 테스트를 수행한다.
go test -run Foo       # "Foo"와 매칭되는 최상위 테스트를 수행한다(예: "TestFooBar")
go test -run Foo/A=    # "Foo"와 매칭되는 최상위 테스트에서, "A="와 매칭되는 서브테스트를 수행한다.
go test -run /A=1      # 모든 최상위 테스트에서, "A="와 매칭되는 서브테스트를 수행한다.
go test -fuzz FuzzFoo  # "FuzzFoo"와 매칭되는 대상에 대해 Fuzz 테스트를 수행한다.

-run 인수는 디버깅을 위해 시드 코퍼스의 특정 값을 실행하는 데 사용할 수도 있다. 예를 들면 다음과 같다.

go test -run=FuzzFoo/9ddb952d9814

-fuzz 및 -run 플래그는, 지정한 대상만 테스트하고 다른 테스트는 skip하기 위해 설정할 수 있다.

서브테스트는 병렬처리 제어에 사용되기도 한다. 상위 테스트는 모든 서브테스트가 완료된 후에 완료된다. 아래 예시에서 테스트는 병렬로 수행되는데, 다른 최상위 테스트와는 무관하고 서로에 대해서만 병렬이다.

func TestGroupedParallel(t *testing.T) {
    for _, tc := range tests {
        tc := tc // range 변수 캡처
        t.Run(tc.Name, func(t *testing.T) {
            t.Parallel()
            ...
        })
    }
}

Run은 병렬 서브테스트들이 모두 완료될 때까지 return되지 않으므로, 한 묶음의 병렬 테스트를 수행한 후에 뒷정리를 할 수 있다.

func TestTeardownParallel(t *testing.T) {
    // 이 Run은 병렬 테스트가 끝날 때까지 return하지 않는다.
    t.Run("group", func(t *testing.T) {
        t.Run("Test1", parallelTest1)
        t.Run("Test2", parallelTest2)
        t.Run("Test3", parallelTest3)
    })
    // <뒷정리 코드>
}

7 Main[ | ]

테스트 또는 벤치마크 프로그램이 실행 전후에 추가 셋업 또는 뒷정리를 수행해야 하는 경우가 있다. 또한 메인 스레드에서 실행되는 코드를 제어해야 하는 경우도 있다. 그런 경우에는 테스트 파일에 다음과 같은 함수를 넣어보자.

func TestMain(m *testing.M)

그러면 생성된 테스트는 테스트/벤치마크를 직접 실행하는 대신 TestMain(m)을 호출한다. TestMain은 메인 고루틴에서 실행되며 m.Run 호출 부근에서 필요한 셋업 및 뒷정리를 할 수 있다. m.Run은 os.Exit로 전달될 수 있는 종료 코드를 return한다. TestMain이 return되면 테스트 래퍼는 m.Run의 결과를 os.Exit에 그대로 전달한다.

TestMain이 호출될 때는, flag.Parse가 실행되지 않았다. TestMain에 testing 패키지의 플래그 등 명령줄 플래그를 적용하려면 flag.Parse를 명시적으로 호출해야 한다. 명령줄 플래그는 항상 테스트/벤치마크 함수가 실행될 때 파싱된다.

TestMain의 간단한 구현 예시는 다음과 같니다.

func TestMain(m *testing.M) {
	// TestMain이 플래그를 사용한다면 여기서 flag.Parse() 호출
	os.Exit(m.Run())
}

TestMain은 저수준 기능으로서, 일반 테스트 함수로 충분한 통상적인 테스트라면 필요하지 않을 수 있다.

8 같이 보기[ | ]

9 참고[ | ]

문서 댓글 ({{ doc_comments.length }})
{{ comment.name }} {{ comment.created | snstime }}