"Using Subtests and Sub-benchmarks"의 두 판 사이의 차이

8번째 줄: 8번째 줄:


==테이블 드리븐 테스트 기본==
==테이블 드리븐 테스트 기본==
자세한 내용을 살펴보기 전에 먼저 Go에서 테스트를 작성하는 일반적인 방법에 대해 논의해 본다. 테스트케이스 조각을 반복하여 일련의 점검을 구현할 수 있다.
자세한 내용을 살펴보기 전에 Go에서 테스트를 작성하는 일반적인 방법에 대해 논의해 본다. 다음과 같이 테스트케이스 조각을 반복하여 일련의 점검을 구현할 수 있다.


<syntaxhighlight lang='go'>
<syntaxhighlight lang='go'>

2023년 6월 8일 (목) 23:57 판

1 개요

Using Subtests and Sub-benchmarks
하위 테스트 및 하위 벤치마크 사용
  • Marcel van Lohuizen 2016-10-03

2 소개

Go 1.7의 testing 패키지에는 하위테스트 및 하위 벤치마크를 생성할 수 있는 T 및 B 타입에 대한 Run 메소드가 도입되었다. 하위테스트 및 하위 벤치마크의 도입으로 오류 처리, 명령줄에서 실행할 테스트에 대한 세밀한 제어, 병렬 처리 제어가 가능하며 종종 더 간단하고 유지관리하기 쉬운 코드가 된다.

3 테이블 드리븐 테스트 기본

자세한 내용을 살펴보기 전에 Go에서 테스트를 작성하는 일반적인 방법에 대해 논의해 본다. 다음과 같이 테스트케이스 조각을 반복하여 일련의 점검을 구현할 수 있다.

func TestTime(t *testing.T) {
    testCases := []struct {
        gmt  string
        loc  string
        want string
    }{
        {"12:31", "Europe/Zuri", "13:31"},     // 잘못된 위치 이름
        {"12:31", "America/New_York", "7:31"}, // 07:31이어야 한다
        {"08:08", "Australia/Sydney", "18:08"},
    }
    for _, tc := range testCases {
        loc, err := time.LoadLocation(tc.loc)
        if err != nil {
            t.Fatalf("could not load location %q", tc.loc)
        }
        gmt, _ := time.Parse("15:04", tc.gmt)
        if got := gmt.In(loc).Format("15:04"); got != tc.want {
            t.Errorf("In(%s, %s) = %s; want %s", tc.gmt, tc.loc, got, tc.want)
        }
    }
}

일반적으로 테이블 드리븐 테스트라고 하는 이 접근 방식은 각 테스트에 대해 동일한 코드를 반복하는 것과 비해, 반복 코드의 양이 줄고 많은 테스트케이스를 추가하는 것이 간단해진다.

4 테이블 기반 벤치마크

Go 1.7 이전에는 벤치마크에 동일한 테이블 기반 접근 방식을 사용할 수 없었다. 벤치마크는 전체 기능의 성능을 테스트하므로 벤치마크를 반복하면 모든 기능이 단일 벤치마크로 측정된다.

일반적인 해결 방법은 각각 다른 매개 변수를 사용하여 공통 함수를 호출하는 별도의 최상위 벤치마크를 정의하는 것이다. 예를 들어 1.7 이전에는 strconv패키지의 벤치마크가 AppendFloat 다음과 같았다.

func benchmarkAppendFloat(b *testing.B, f float64, fmt byte, prec, bitSize int) {

   dst := make([]byte, 30)
   b.ResetTimer() // Overkill here, but for illustrative purposes.
   for i := 0; i < b.N; i++ {
       AppendFloat(dst[:0], f, fmt, prec, bitSize)
   }

}

func BenchmarkAppendFloatDecimal(b *testing.B) { benchmarkAppendFloat(b, 33909, 'g', -1, 64) } func BenchmarkAppendFloat(b *testing.B) { benchmarkAppendFloat(b, 339.7784, 'g', -1, 64) } func BenchmarkAppendFloatExp(b *testing.B) { benchmarkAppendFloat(b, -5.09e75, 'g', -1, 64) } func BenchmarkAppendFloatNegExp(b *testing.B) { benchmarkAppendFloat(b, -5.11e-95, 'g', -1, 64) } func BenchmarkAppendFloatBig(b *testing.B) { benchmarkAppendFloat(b, 123456789123456789123456789, 'g', -1, 64) } ... Go 1.7에서 사용할 수 있는 방법을 사용하면 Run동일한 벤치마크 세트가 단일 최상위 벤치마크로 표현된다.

func BenchmarkAppendFloat(b *testing.B) {

   benchmarks := []struct{
       name    string
       float   float64
       fmt     byte
       prec    int
       bitSize int
   }{
       {"Decimal", 33909, 'g', -1, 64},
       {"Float", 339.7784, 'g', -1, 64},
       {"Exp", -5.09e75, 'g', -1, 64},
       {"NegExp", -5.11e-95, 'g', -1, 64},
       {"Big", 123456789123456789123456789, 'g', -1, 64},
       ...
   }
   dst := make([]byte, 30)
   for _, bm := range benchmarks {
       b.Run(bm.name, func(b *testing.B) {
           for i := 0; i < b.N; i++ {
               AppendFloat(dst[:0], bm.float, bm.fmt, bm.prec, bm.bitSize)
           }
       })
   }

} 메서드를 호출할 때마다 Run별도의 벤치마크가 생성된다. 메서드를 호출하는 둘러싸는 벤치마크 함수는 Run한 번만 실행되며 측정되지 않는다.

새 코드에는 더 많은 코드 줄이 있지만 유지 관리가 더 쉽고 가독성이 높으며 테스트에 일반적으로 사용되는 테이블 기반 접근 방식과 일치한다. 또한 타이머를 재설정할 필요 없이 실행 간에 공통 설정 코드가 공유된다.

5 하위 테스트를 사용하는 테이블 기반 테스트

Go 1.7에는 Run하위 테스트를 만드는 방법도 도입되었다. 이 테스트는 하위 테스트를 사용하여 이전 예제를 다시 작성한 버전이다.

func TestTime(t *testing.T) {

   testCases := []struct {
       gmt  string
       loc  string
       want string
   }{
       {"12:31", "Europe/Zuri", "13:31"},
       {"12:31", "America/New_York", "7:31"},
       {"08:08", "Australia/Sydney", "18:08"},
   }
   for _, tc := range testCases {
       t.Run(fmt.Sprintf("%s in %s", tc.gmt, tc.loc), func(t *testing.T) {
           loc, err := time.LoadLocation(tc.loc)
           if err != nil {
               t.Fatal("could not load location")
           }
           gmt, _ := time.Parse("15:04", tc.gmt)
           if got := gmt.In(loc).Format("15:04"); got != tc.want {
               t.Errorf("got %s; want %s", got, tc.want)
           }
       })
   }

} 가장 먼저 주목해야 할 것은 두 구현의 출력 차이이다. 원래 구현은 다음과 같이 프린트한다.

--- FAIL: TestTime (0.00s)

   time_test.go:62: could not load location "Europe/Zuri"

두 가지 오류가 있더라도 에 대한 호출에서 테스트 실행이 중단되고 Fatalf두 번째 테스트는 실행되지 않는다.

다음을 모두 인쇄 하는 구현 Run:

--- FAIL: TestTime (0.00s)

   --- FAIL: TestTime/12:31_in_Europe/Zuri (0.00s)
       time_test.go:84: could not load location
   --- FAIL: TestTime/12:31_in_America/New_York (0.00s)
       time_test.go:88: got 07:31; want 7:31

Fatal형제 요소로 인해 하위 테스트는 건너뛰지만 상위 또는 후속 하위 테스트는 건너뛸 수 없다.

주목해야 할 또 다른 사항은 새 구현에서 더 짧은 오류 메시지이다. 하위 테스트 이름은 하위 테스트를 고유하게 식별하므로 오류 메시지 내에서 테스트를 다시 식별할 필요가 없다.

다음 섹션에서 설명하는 것처럼 하위 테스트 또는 하위 벤치마크를 사용하면 여러 가지 다른 이점이 있다.

6 특정 테스트 또는 벤치마크 실행

-run하위 테스트와 하위 벤치마크는 모두 또는 -bench플래그를 사용하여 명령줄에서 구분할 수 있다 . 두 플래그 모두 하위 테스트 또는 하위 벤치마크의 전체 이름 중 해당 부분과 일치하는 슬래시로 구분된 정규식 목록을 사용한다.

하위 테스트 또는 하위 벤치마크의 전체 이름은 슬래시로 구분된 해당 이름과 최상위 레벨부터 시작하는 모든 상위 이름의 목록이다. 이름은 최상위 테스트 및 벤치마크에 해당하는 함수 이름이며 Run그렇지 않은 경우의 첫 번째 인수이다. 표시 및 구문 분석 문제를 방지하기 위해 공백을 밑줄로 바꾸고 인쇄할 수 없는 문자를 이스케이프 처리하여 이름을 삭제한다. -run또는 플래그 에 전달된 정규식에 동일한 삭제가 적용된다 -bench.

몇 가지 예:

유럽 ​​시간대를 사용하는 테스트 실행:

$ go test -run=TestTime/"in Europe" --- FAIL: TestTime (0.00s)

   --- FAIL: TestTime/12:31_in_Europe/Zuri (0.00s)
       time_test.go:85: could not load location

정오 이후 시간에만 테스트 실행:

$ go test -run=Time/12:[0-9] -v === RUN TestTime === RUN TestTime/12:31_in_Europe/Zuri === RUN TestTime/12:31_in_America/New_York --- FAIL: TestTime (0.00s)

   --- FAIL: TestTime/12:31_in_Europe/Zuri (0.00s)
       time_test.go:85: could not load location
   --- FAIL: TestTime/12:31_in_America/New_York (0.00s)
       time_test.go:89: got 07:31; want 7:31

아마도 조금 놀라운 점은 using 이 -run=TestTime/New_York어떤 테스트와도 일치하지 않는다는 것이다. 위치 이름에 있는 슬래시도 구분 기호로 취급되기 때문이다. 대신 다음을 사용하십시오.

$ go test -run=Time//New_York --- FAIL: TestTime (0.00s)

   --- FAIL: TestTime/12:31_in_America/New_York (0.00s)
       time_test.go:88: got 07:31; want 7:31

//에 전달된 문자열의 에 유의하자 -run. /시간대 내 이름은 하위 America/New_York테스트에서 생성된 구분 기호인 것처럼 처리된다. 패턴( TestTime)의 첫 번째 정규식은 최상위 테스트와 일치한다. 두 번째 정규식(빈 문자열)은 무엇이든 일치한다. 이 경우에는 위치의 시간과 대륙 부분이다. 세 번째 정규식( New_York)은 위치의 도시 부분과 일치한다.

이름의 슬래시를 구분 기호로 취급하면 사용자가 이름을 변경할 필요 없이 테스트 계층 구조를 리팩터링할 수 있다. 또한 이스케이프 규칙을 단순화한다. 사용자는 이름에서 슬래시를 이스케이프 처리해야 한다. 예를 들어 이것이 문제가 될 경우 백슬래시로 교체한다.

고유하지 않은 테스트 이름에는 고유한 시퀀스 번호가 추가된다. Run 따라서 하위 테스트에 대한 명확한 명명 체계가 없고 하위 테스트를 시퀀스 번호로 쉽게 식별할 수 있는 경우 빈 문자열을 전달할 수 있다 .

7 설정 및 분해

하위 테스트 및 하위 벤치마크를 사용하여 공통 설정 및 해제 코드를 관리할 수 있다.

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) {
       if !test(foo{B:1}) {
           t.Fail()
       }
   })
   // <tear-down code>

} 동봉된 하위 테스트 중 하나라도 실행되면 설정 및 해제 코드가 실행되며 최대 한 번만 실행된다. 이는 하위 테스트 중 하나가 Skip, Fail또는 를 호출하는 경우에도 적용된다 Fatal.

8 병렬성 제어

하위 테스트를 통해 병렬 처리를 세밀하게 제어할 수 있다. 하위 테스트를 사용하는 방법을 이해하려면 병렬 테스트의 의미를 이해하는 것이 중요하다.

각 테스트는 테스트 기능과 연결된다. 테스트 함수가 의 인스턴스에서 Parallel 메서드를 호출하는 경우 테스트를 병렬 테스트라고 한다 testing.T. 병렬 테스트는 순차 테스트와 동시에 실행되지 않으며 상위 테스트의 호출 테스트 기능이 반환될 때까지 실행이 일시 중단된다. 이 -parallel플래그는 병렬로 실행할 수 있는 최대 병렬 테스트 수를 정의한다.

테스트 기능이 반환되고 모든 하위 테스트가 완료될 때까지 테스트가 차단된다. 이는 순차 테스트에 의해 실행되는 병렬 테스트가 다른 연속 순차 테스트가 실행되기 전에 완료됨을 의미한다.

Run이 동작은 만든 테스트 와 최상위 수준 테스트 에서 동일하다 . 실제로 내부적으로 최상위 수준 테스트는 숨겨진 마스터 테스트의 하위 테스트로 구현된다.

8.1 테스트 그룹을 병렬로 실행

위의 의미는 테스트 그룹을 서로 병렬로 실행할 수 있지만 다른 병렬 테스트와 함께 실행할 수는 없다.

func TestGroupedParallel(t *testing.T) {

   for _, tc := range testCases {
       tc := tc // capture range variable
       t.Run(tc.Name, func(t *testing.T) {
           t.Parallel()
           if got := foo(tc.in); got != tc.out {
               t.Errorf("got %v; want %v", got, tc.out)
           }
           ...
       })
   }

} 외부 테스트는 에서 시작된 모든 병렬 테스트가 Run 완료될 때까지 완료되지 않는다. 결과적으로 다른 병렬 테스트는 이러한 병렬 테스트와 병렬로 실행할 수 없다.

tc올바른 인스턴스에 바인딩되도록 하려면 범위 변수를 캡처해야 한다 .

8.2 병렬 테스트 그룹 후 정리

이전 예제에서 우리는 다른 테스트를 시작하기 전에 병렬 테스트 그룹이 완료되기를 기다리는 시맨틱을 사용했다. 동일한 기술을 사용하여 공통 리소스를 공유하는 병렬 테스트 그룹을 정리할 수 있다.

func TestTeardownParallel(t *testing.T) {

   // <setup code>
   // This Run will not return until its parallel subtests complete.
   t.Run("group", func(t *testing.T) {
       t.Run("Test1", parallelTest1)
       t.Run("Test2", parallelTest2)
       t.Run("Test3", parallelTest3)
   })
   // <tear-down code>

} 병렬 테스트 그룹을 기다리는 동작은 이전 예제와 동일하다.

9 결론

Go 1.7에 추가된 하위 테스트 및 하위 벤치마크를 통해 기존 도구와 잘 어울리는 자연스러운 방식으로 구조화된 테스트 및 벤치마크를 작성할 수 있다. 이에 대해 생각할 수 있는 한 가지 방법은 테스트 패키지의 이전 버전에 1레벨 계층 구조가 있다는 것이다. 즉, 패키지 레벨 테스트는 일련의 개별 테스트 및 벤치마크로 구성되었다. 이제 해당 구조가 재귀적으로 개별 테스트 및 벤치마크로 확장되었다. 실제로 구현 시 최상위 테스트 및 벤치마크는 암시적 마스터 테스트 및 벤치마크의 하위 테스트 및 하위 벤치마크인 것처럼 추적된다. 치료는 실제로 모든 수준에서 동일하다.

이 구조를 정의하는 테스트 기능을 통해 특정 테스트 케이스의 세분화된 실행, 공유 설정 및 해제, 테스트 병렬 처리에 대한 더 나은 제어가 가능하다. 우리는 사람들이 다른 용도를 찾는 것을 보게 되어 기쁘다.

10 같이 보기

11 참고

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