통합 테스트용 커버리지 프로파일링 지원
통합 테스트용 커버리지 프로파일링 지원 (Coverage profiling support for integration tests)
Go 1.20부터 Go는 커버리지 프로파일(coverage profile)을 애플리케이션과 통합 테스트(integration test) — Go 프로그램을 위한 더 크고 복잡한 테스트 — 에서 수집할 수 있게 지원합니다.
Go는 패키지 유닛 테스트 수준에서 "go test -coverprofile=... <pkg_target>" 명령으로 커버리지 프로파일을 수집하는 손쉬운 지원을 제공해 왔어요. Go 1.20부터는 더 큰 통합 테스트 — 주어진 애플리케이션 바이너리를 여러 번 실행하는 더 무겁고 복잡한 테스트 — 를 위해 커버리지 프로파일을 수집할 수도 있습니다.
출처: Go 공식 문서
유닛 테스트에서 커버리지 프로파일을 수집하고 리포트를 만들려면 두 단계가 필요해요. go test -coverprofile=... 실행 후에, 리포트를 생성하는 go tool cover {-func,-html} 호출이 따르는 방식입니다.
통합 테스트에서는 다음에서 설명하는 대로 빌드 단계, 실행 단계(빌드 단계의 바이너리를 여러 번 호출하는 것을 포함할 수 있어요), 마지막으로 리포트 단계, 이렇게 세 단계가 필요합니다.
커버리지 프로파일링용 바이너리 빌드하기
커버리지 프로파일을 수집하기 위한 애플리케이션을 빌드하려면 애플리케이션 바이너리 타깃에 go build를 호출할 때 -cover 플래그를 넘기세요. 샘플 go build -cover 호출은 아래 섹션에서 볼 수 있어요. 결과 바이너리는 환경 변수 설정으로 실행해서 커버리지 프로파일을 포착할 수 있습니다(실행 섹션 참고).
계측(instrumentation)에 포함될 패키지 선택 방법
"go build -cover" 실행 동안 Go 명령은 메인 모듈의 패키지들을 커버리지 프로파일링 대상으로 선택합니다. 빌드에 들어가는 다른 패키지들(go.mod에 나열된 의존성이나 Go 표준 라이브러리의 일부인 패키지들)은 기본적으로 포함되지 않아요.
예를 들어 main 패키지, 로컬 메인 모듈 패키지 greetings, 그리고 모듈 밖에서 import 하는 패키지들(rsc.io/quote, fmt 등을 포함)이 있는 예시 프로그램이 있다고 해 볼게요 (전체 프로그램 링크).
$ cat go.mod
module mydomain.com
go 1.20
require rsc.io/quote v1.5.2
require (
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c // indirect
rsc.io/sampler v1.3.0 // indirect
)
$ cat myprogram.go
package main
import (
"fmt"
"mydomain.com/greetings"
"rsc.io/quote"
)
func main() {
fmt.Printf("I say %q and %q\n", quote.Hello(), greetings.Goodbye())
}
$ cat greetings/greetings.go
package greetings
func Goodbye() string {
return "see ya"
}
$ go build -cover -o myprogram.exe .
$
이 프로그램을 "-cover" 커맨드라인 플래그로 빌드하고 실행하면 정확히 두 패키지, main과 mydomain.com/greetings가 프로파일에 포함됩니다. 다른 의존 패키지들은 제외돼요.
커버리지에 포함할 패키지를 더 세밀하게 통제하고 싶은 사용자는 "-coverpkg" 플래그로 빌드할 수 있어요. 예시:
$ go build -cover -o myprogramMorePkgs.exe -coverpkg=io,mydomain.com,rsc.io/quote .
$
위 빌드에서 mydomain.com의 main 패키지와 rsc.io/quote, io 패키지가 프로파일링 대상으로 선택됩니다. mydomain.com/greetings는 명시적으로 나열되지 않았으므로, 메인 모듈에 있음에도 프로파일에서 제외돼요.
커버리지 계측된 바이너리 실행하기
"-cover"로 빌드된 바이너리는 실행이 끝날 때 GOCOVERDIR 환경 변수로 지정된 디렉터리에 프로파일 데이터 파일을 씁니다. 예시:
$ go build -cover -o myprogram.exe myprogram.go
$ mkdir somedata
$ GOCOVERDIR=somedata ./myprogram.exe
I say "Hello, world." and "see ya"
$ ls somedata
covcounters.c6de772f99010ef5925877a7b05db4cc.2424989.1670252383678349347
covmeta.c6de772f99010ef5925877a7b05db4cc
$
somedata 디렉터리에 쓰인 두 파일에 주목하세요. 이 (바이너리) 파일들이 커버리지 결과를 담고 있습니다. 이 데이터 파일들에서 사람이 읽을 수 있는 결과를 만드는 방법에 대해서는 아래 리포트 섹션을 참고하세요.
GOCOVERDIR 환경 변수가 설정되지 않으면 커버리지 계측된 바이너리는 여전히 올바르게 실행되지만 경고를 내보냅니다. 예시:
$ ./myprogram.exe
warning: GOCOVERDIR not set, no coverage data emitted
I say "Hello, world." and "see ya"
$
여러 번 실행을 포함하는 테스트
통합 테스트는 많은 경우 여러 번의 프로그램 실행을 포함할 수 있어요. 프로그램이 "-cover"로 빌드되면 각 실행이 새 데이터 파일을 만듭니다. 예시:
$ mkdir somedata2
$ GOCOVERDIR=somedata2 ./myprogram.exe // first run
I say "Hello, world." and "see ya"
$ GOCOVERDIR=somedata2 ./myprogram.exe -flag // second run
I say "Hello, world." and "see ya"
$ ls somedata2
covcounters.890814fca98ac3a4d41b9bd2a7ec9f7f.2456041.1670259309405583534
covcounters.890814fca98ac3a4d41b9bd2a7ec9f7f.2456047.1670259309410891043
covmeta.890814fca98ac3a4d41b9bd2a7ec9f7f
$
커버리지 데이터 출력 파일은 두 종류가 있어요. 실행마다 변하지 않는 항목(소스 파일 이름, 함수 이름 같은)을 담은 메타데이터 파일과, 실행된 프로그램 부분을 기록하는 카운터 데이터 파일이죠.
위 예시에서 첫 번째 실행은 두 파일(카운터와 메타)을 만들었고, 두 번째 실행은 카운터 데이터 파일만 만들었어요. 메타데이터는 실행마다 바뀌지 않으므로 한 번만 쓰면 되기 때문입니다.
커버리지 데이터 파일 다루기
Go 1.20은 GOCOVERDIR 디렉터리에서 커버리지 데이터 파일을 읽고 조작하는 데 쓸 수 있는 새 도구 covdata를 도입했습니다.
Go의 covdata 도구는 다양한 모드로 동작합니다. covdata 도구 호출의 일반적인 형태는 다음과 같아요:
$ go tool covdata <mode> -i=<dir1,dir2,...> ...flags...
여기서 "-i" 플래그는 읽을 디렉터리 목록을 제공하며, 각 디렉터리는 커버리지 계측된 바이너리 실행(GOCOVERDIR을 통해)에서 파생됩니다.
커버리지 프로파일 리포트 만들기
이 섹션은 "go tool covdata"로 커버리지 데이터 파일에서 사람이 읽을 수 있는 리포트를 만드는 방법을 다룹니다.
문장 커버리지 비율 보고
각 계측된 패키지에 대해 "문장 커버리지 비율(percent statements covered)" 지표를 보고하려면 "go tool covdata percent -i=<directory>" 명령을 사용하세요. 위 실행 섹션의 예시를 사용하면:
$ ls somedata
covcounters.c6de772f99010ef5925877a7b05db4cc.2424989.1670252383678349347
covmeta.c6de772f99010ef5925877a7b05db4cc
$ go tool covdata percent -i=somedata
main coverage: 100.0% of statements
mydomain.com/greetings coverage: 100.0% of statements
$
여기서 "문장 커버리지" 비율은 go test -cover가 보고하는 것과 정확히 일치합니다.
레거시 텍스트 형식으로 변환하기
covdata의 textfmt 선택자로 바이너리 커버리지 데이터 파일을 "go test -coverprofile=<outfile>"이 생성하는 레거시 텍스트 형식으로 변환할 수 있어요. 결과 텍스트 파일은 이후 "go tool cover -func"나 "go tool cover -html"과 함께 사용해서 추가 리포트를 만들 수 있습니다. 예시:
$ ls somedata
covcounters.c6de772f99010ef5925877a7b05db4cc.2424989.1670252383678349347
covmeta.c6de772f99010ef5925877a7b05db4cc
$ go tool covdata textfmt -i=somedata -o profile.txt
$ cat profile.txt
mode: set
mydomain.com/myprogram.go:10.13,12.2 1 1
mydomain.com/greetings/greetings.go:3.23,5.2 1 1
$ go tool cover -func=profile.txt
mydomain.com/greetings/greetings.go:3: Goodbye 100.0%
mydomain.com/myprogram.go:10: main 100.0%
total: (statements) 100.0%
$
병합하기 (Merging)
"go tool covdata"의 merge 하위 명령으로 여러 데이터 디렉터리의 프로파일을 병합할 수 있어요.
예를 들어 macOS와 Windows 양쪽에서 실행되는 프로그램을 생각해 봅시다. 이런 프로그램의 작성자는 각 운영체제에서의 별도 실행 커버리지 프로파일을 단일 프로파일 묶음으로 합쳐서, 크로스 플랫폼 커버리지 요약을 만들고 싶을 수 있어요. 예를 들어:
$ ls windows_datadir
covcounters.f3833f80c91d8229544b25a855285890.1025623.1667481441036838252
covcounters.f3833f80c91d8229544b25a855285890.1025628.1667481441042785007
covmeta.f3833f80c91d8229544b25a855285890
$ ls macos_datadir
covcounters.b245ad845b5068d116a4e25033b429fb.1025358.1667481440551734165
covcounters.b245ad845b5068d116a4e25033b429fb.1025364.1667481440557770197
covmeta.b245ad845b5068d116a4e25033b429fb
$ ls macos_datadir
$ mkdir merged
$ go tool covdata merge -i=windows_datadir,macos_datadir -o merged
$
위 병합 연산은 지정된 입력 디렉터리의 데이터를 합쳐서 "merged" 디렉터리에 새 병합 데이터 파일 집합을 씁니다.
패키지 선택 (Package selection)
대부분의 "go tool covdata" 명령은 연산의 일부로 패키지 선택을 수행하는 "-pkg" 플래그를 지원합니다. "-pkg"의 인자는 Go 명령의 "-coverpkg" 플래그가 쓰는 형식과 동일합니다. 예시:
$ ls somedata
covcounters.c6de772f99010ef5925877a7b05db4cc.2424989.1670252383678349347
covmeta.c6de772f99010ef5925877a7b05db4cc
$ go tool covdata percent -i=somedata -pkg=mydomain.com/greetings
mydomain.com/greetings coverage: 100.0% of statements
$ go tool covdata percent -i=somedata -pkg=nonexistentpackage
$
"-pkg" 플래그는 특정 리포트에 관심 있는 패키지의 부분집합을 선택하는 데 쓸 수 있어요.
자주 묻는 질문 (Frequently Asked Questions)
- go.mod 파일에 언급된 모든 import 패키지에 커버리지 계측을 요청하려면?
- GOPATH/GO111MODULE=off 모드에서
go build -cover를 쓸 수 있나요? - 프로그램이 panic 하면 커버리지 데이터가 쓰여지나요?
-coverpkg=main이 내 main 패키지를 프로파일링 대상으로 선택하나요?
go.mod 파일에 언급된 모든 import 패키지에 커버리지 계측을 요청하려면?
기본적으로 go build -cover는 모든 메인 모듈 패키지를 커버리지 대상으로 계측하지만, 메인 모듈 밖의 import(예: 표준 라이브러리 패키지나 go.mod에 나열된 import)는 계측하지 않아요. 표준 라이브러리가 아닌 모든 의존성에 계측을 요청하는 한 가지 방법은 go list의 출력을 -coverpkg에 넣는 것입니다. 위에서 인용한 예시 프로그램을 다시 사용한 예시입니다:
$ go list -f '{{if not .Standard}}{{.ImportPath}}{{end}}' -deps . | paste -sd "," > pkgs.txt
$ go build -o myprogram.exe -coverpkg=`cat pkgs.txt` .
$ mkdir somedata
$ GOCOVERDIR=somedata ./myprogram.exe
$ go tool covdata percent -i=somedata
golang.org/x/text/internal/tag coverage: 78.4% of statements
golang.org/x/text/language coverage: 35.5% of statements
mydomain.com coverage: 100.0% of statements
mydomain.com/greetings coverage: 100.0% of statements
rsc.io/quote coverage: 25.0% of statements
rsc.io/sampler coverage: 86.7% of statements
$
GO111MODULE=off 모드에서 go build -cover를 쓸 수 있나요?
네, go build -cover는 GO111MODULE=off에서도 동작합니다. GO111MODULE=off 모드로 프로그램을 빌드하면 명령줄에서 타깃으로 정확히 이름이 지정된 패키지만 프로파일링 대상으로 계측돼요. 추가 패키지를 프로파일에 포함하려면 -coverpkg 플래그를 사용하세요.
프로그램이 panic 하면 커버리지 데이터가 쓰여지나요?
go build -cover로 빌드된 프로그램은 실행 끝에, 프로그램이 os.Exit()를 호출하거나 main.main에서 정상적으로 반환할 때만 완전한 프로파일 데이터를 씁니다. 프로그램이 복구되지 않은 panic으로 종료되거나 치명적 예외(세그멘테이션 위반, 0 나누기 등)에 부딪히면, 실행 중 실행된 문장의 프로파일 데이터는 유실됩니다.
-coverpkg=main이 내 main 패키지를 프로파일링 대상으로 선택하나요?
-coverpkg 플래그는 패키지 이름 목록이 아니라 import 경로 목록을 받습니다. main 패키지를 커버리지 계측 대상으로 선택하려면 이름이 아니라 import 경로로 식별하세요. 예시(이 예시 프로그램 사용):
$ go list -m
mydomain.com
$ go build -coverpkg=main -o oops.exe .
warning: no packages being built depend on matches for pattern main
$ go build -coverpkg=mydomain.com -o myprogram.exe .
$ mkdir somedata
$ GOCOVERDIR=somedata ./myprogram.exe
I say "Hello, world." and "see ya"
$ go tool covdata percent -i=somedata
mydomain.com coverage: 100.0% of statements
$
리소스 (Resources)
- Go 1.2의 유닛 테스트 커버리지를 소개하는 블로그 글: 유닛 테스트 커버리지 프로파일링은 Go 1.2 릴리스의 일부로 도입됐어요. 자세한 내용은 이 블로그 글을 보세요.
- 문서:
cmd/go패키지 문서가 커버리지와 관련된 빌드·테스트 플래그를 설명합니다. - 기술 세부 사항:
용어집 (Glossary)
unit test(유닛 테스트): 특정 Go 패키지와 연관된 *_test.go 파일 안의 테스트로, Go의 testing 패키지를 사용합니다.
integration test(통합 테스트): 주어진 애플리케이션 또는 바이너리에 대한 더 포괄적이고 무거운 테스트. 통합 테스트는 대개 프로그램(또는 프로그램 집합)을 빌드한 뒤, Go의 testing 패키지를 기반으로 하거나 그렇지 않을 수 있는 테스트 하니스(harness)의 통제 아래 여러 입력과 시나리오로 프로그램들을 일련의 실행으로 수행합니다.
더 알아보기 (Learn more)
cmd/go패키지 문서 — 커버리지 관련 빌드·테스트 플래그- 블로그 글: The cover story — Go 1.2의 유닛 테스트 커버리지 소개
- Design draft — 커버리지 재설계 설계 초안
- Proposal — 관련 제안