Go 통합 테스트를 위한 코드 커버리지

1 개요[ | ]

Code coverage for Go integration tests
Go 통합 테스트를 위한 코드 커버리지
  • Than McIntosh 2023-03-08

코드 커버리지 도구는 개발자가 주어진 테스트 스위트가 실행될 때 소스 코드 베이스의 어느 부분이 실행(커버)되는지 확인하는 데 도움이 된다.

Go는 "go test" 명령어의 "-cover" 플래그를 사용하여 패키지 수준에서 코드 커버리지를 측정하기 위해 한동안 지원(Go 1.2 릴리스에 도입)을 제공했다.

이 도구는 대부분의 경우 잘 작동하지만 더 큰 Go 애플리케이션에는 몇 가지 약점이 있다. 그런 애플리케이션의 경우 개발자는 전체 프로그램의 동작을 확인하는 "통합" 테스트를 작성하는 경우가 많다(패키지 수준 단위 테스트 외에도).

이러한 유형의 테스트에서는, 개별 패키지를 격리하여 테스트하는 것는 반대로, 일반적으로 완전한 애플리케이션 바이너리를 빌드한 다음 대표적인 입력 세트(또는 서버라면 프로덕션 로드에서)에서 바이너리를 실행하여 모든 컴포넌트 패키지가 올바르게 함께 작동하는지 확인한다.

통합 테스트 바이너리는 "go test"가 아닌 "go build"로 빌드되었기 때문에 Go의 도구는 지금까지 이러한 테스트에 대한 커버리지 프로파일을 수집하는 쉬운 방법을 제공하지 않았다.

Go 1.20에서는 이제 "go build -cover"를 사용하여 커버리지 계측 프로그램을 빌드한 다음, 이러한 계측 바이너리를 통합 테스트에 공급하여 커버리지 테스트 범위를 확장할 수 있다.

이 글에서는 이러한 새로운 기능의 작동 방식에 대한 예시를 제공하고, 통합 테스트에서 커버리지 프로파일을 수집하기 위한 일부 사용사례 및 워크플로우를 간략하게 설명한다.

1.1 Example[ | ]

아주 작은 예제 프로그램을 가지고 간단한 통합 테스트를 작성한 다음 통합 테스트에서 커버리지 프로파일을 수집해보자.

이 연습에서는 gitlab.com/golang-commonmark/mdtool의 "mdtool" 마크다운 처리 도구를 사용한다. 이것은 클라이언트가 markdown-to-HTML 변환 라이브러리인 gitlab.com/golang-commonmark/markdown 패키지를 사용하는 방법을 보여주기 위해 설계된 데모 프로그램이다.

1.2 mdtool 셋업[ | ]

먼저 "mdtool" 자체의 사본을 다운로드한다(이 단계를 재현할 수 있도록 특정 버전을 선택한다).

$ git clone https://gitlab.com/golang-commonmark/mdtool.git
...
$ cd mdtool
$ git tag example e210a4502a825ef7205691395804eefce536a02f
$ git checkout example
...
$

1.3 간단한 통합 테스트[ | ]

이제 "mdtool"에 대한 간단한 통합 테스트를 작성할 것이다. 테스트는 "mdtool" 바이너리를 빌드한 다음 일련의 입력 마크다운 파일에서 실행한다. 아래의 간단한 스크립트는 테스트 데이터 디렉터리의 각 파일에서 "mdtool" 바이너리를 실행하여 일부 출력을 생성하고 crash가 발생하지 않는지 확인한다.

$ cat integration_test.sh
#!/bin/sh
BUILDARGS="$*"
#
# Terminate the test if any command below does not complete successfully.
#
set -e
#
# Download some test inputs (the 'website' repo contains various *.md files).
#
if [ ! -d testdata ]; then
  git clone https://go.googlesource.com/website testdata
  git -C testdata tag example 8bb4a56901ae3b427039d490207a99b48245de2c
  git -C testdata checkout example
fi
#
# Build mdtool binary for testing purposes.
#
rm -f mdtool.exe
go build $BUILDARGS -o mdtool.exe .
#
# Run the tool on a set of input files from 'testdata'.
#
FILES=$(find testdata -name "*.md" -print)
N=$(echo $FILES | wc -w)
for F in $FILES
do
  ./mdtool.exe +x +a $F > /dev/null
done
echo "finished processing $N files, no crashes"
$

다음은 테스트 실행 예시이다.

$ /bin/sh integration_test.sh
...
finished processing 380 files, no crashes
$

성공: "mdtool" 바이너리가 일련의 입력 파일을 성공적으로 처리했음을 확인했다... 하지만 실제로 실행한 도구의 소스 코드는 얼마나 될까? 다음 섹션에서는 그것을 알아보기 위해 커버리지 프로파일을 수집한다.

1.4 커버리지 데이터를 수집하는 통합 테스트 사용하기[ | ]

이전 스크립트를 호출하지만, 커버리지를 위한 도구를 빌드한 후 결과 프로파일을 후처리하는 래퍼 스크립트를 작성해보자.

$ cat wrap_test_for_coverage.sh
#!/bin/sh
set -e
PKGARGS="$*"
#
# Setup
#
rm -rf covdatafiles
mkdir covdatafiles
#
# Pass in "-cover" to the script to build for coverage, then
# run with GOCOVERDIR set.
#
GOCOVERDIR=covdatafiles \
  /bin/sh integration_test.sh -cover $PKGARGS
#
# Post-process the resulting profiles.
#
go tool covdata percent -i=covdatafiles
$

위 래퍼에서 살펴볼 핵심사항은 다음과 같다.

  • integration_test.sh를 실행할 때 "-cover" 플래그를 전달하며, 커버리지 계측 "mdtool.exe" 바이너리가 제공된다.
  • GOCOVERDIR 환경변수를 커버리지 데이터 파일이 기록될 디렉토리로 설정한다.
  • 테스트가 완료되면 "go tool covdata percent"를 실행하여 적용되는 명령문의 백분율에 대한 보고서를 생성한다.

다음은 이 래퍼 스크립트를 실행했을 때의 출력이다.

$ /bin/sh wrap_test_for_coverage.sh
...
    gitlab.com/golang-commonmark/mdtool coverage: 48.1% of statements
$
# Note: covdatafiles now contains 381 files.

짠! 이제 "mdtool" 애플리케이션의 소스코드를 실행하는 데 통합 테스트가 얼마나 잘 작동하는지 어느 정도 알 수 있다.

테스트 도구를 개선하기 위해 변경한 다음 두 번째 커버리지 수집 실행을 수행하면 커버리지 보고서에 변경사항이 반영된 것을 볼 수 있다. 예를 들어 다음 두 줄을 integration_test.sh에 추가하여 테스트를 개선한다고 생각해보자.

./mdtool.exe +ty testdata/README.md  > /dev/null
./mdtool.exe +ta < testdata/README.md  > /dev/null

커버리지 테스트 래퍼를 다시 실행한다.

$ /bin/sh wrap_test_for_coverage.sh
finished processing 380 files, no crashes
    gitlab.com/golang-commonmark/mdtool coverage: 54.6% of statements
$

변경의 효과를 볼 수 있다. 커버리지가 48%에서 54%로 증가했다.

1.5 커버할 패키지 선택[ | ]

기본적으로 "go build -cover"는 빌드 중인 Go 모듈의 일부인 패키지만 계측한다. 이 경우에는 gitlab.com/golang-commonmark/mdtool 패키지이다. 그러나 어떤 경우에는 커버리지 계측을 다른 패키지로 확장하는 것이 유용하다. 이는 "-coverpkg"를 "go build -cover"에 전달하여 수행할 수 있다.

예제 프로그램의 경우 "mdtool"은 실제로 gitlab.com/golang-commonmark/markdown 패키지를 둘러싼 래퍼일 뿐인데, 계측된 패키지 세트에 markdown이 포함되는 것이 흥미롭다.

다음은 "mdtool"에 대한 go.mod 파일이다.

$ head go.mod
module gitlab.com/golang-commonmark/mdtool

go 1.17

require (
    github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
    gitlab.com/golang-commonmark/markdown v0.0.0-20211110145824-bf3e522c626a
)

"-coverpkg" 플래그를 사용하여, 위의 의존성 패키지들 중에서 커버리지 분석에 포함시킬 것을 선택할 수 있다. 예를 들면 다음과 같다.

$ /bin/sh wrap_test_for_coverage.sh -coverpkg=gitlab.com/golang-commonmark/markdown,gitlab.com/golang-commonmark/mdtool
...
    gitlab.com/golang-commonmark/markdown   coverage: 70.6% of statements
    gitlab.com/golang-commonmark/mdtool coverage: 54.6% of statements
$

2 커버리지 데이터 파일 작업[ | ]

커버리지 통합 테스트가 완료되고 원시 데이터 파일 세트(여기서는 covdatafiles 디렉토리의 내용물)를 작성하면 다양한 방식으로 이러한 파일을 후처리할 수 있다.

2.1 프로파일을 ‘-coverprofile’ 텍스트 포맷으로 변환[ | ]

단위 테스트 작업시, go test -coverprofile=abc.txt를 실행하여 텍스트 포맷 커버리지 프로파일을 작성할 수 있다 .

go build -cover로 빌드된 바이너리를 사용하면, GOCOVERDIR 디렉터리로 내보낸 파일에서 go tool covdata textfmt를 실행하여 사후에 텍스트 포맷 프로파일을 생성할 수 있다.

이 단계가 완료되면, go test -coverprofile과 마찬가지로, go tool cover -func=<file>또는 go tool cover -html=<file>를 사용하여 데이터를 해석/시각화할 수 있다.

예시:

$ /bin/sh wrap_test_for_coverage.sh
...
$ go tool covdata textfmt -i=covdatafiles -o=cov.txt
$ go tool cover -func=cov.txt
gitlab.com/golang-commonmark/mdtool/main.go:40:     readFromStdin   100.0%
gitlab.com/golang-commonmark/mdtool/main.go:44:     readFromFile    80.0%
gitlab.com/golang-commonmark/mdtool/main.go:54:     readFromWeb 0.0%
gitlab.com/golang-commonmark/mdtool/main.go:64:     readInput   80.0%
gitlab.com/golang-commonmark/mdtool/main.go:74:     extractText 100.0%
gitlab.com/golang-commonmark/mdtool/main.go:88:     writePreamble   100.0%
gitlab.com/golang-commonmark/mdtool/main.go:111:    writePostamble  100.0%
gitlab.com/golang-commonmark/mdtool/main.go:118:    handler     0.0%
gitlab.com/golang-commonmark/mdtool/main.go:139:    main        51.6%
total:                          (statements)    54.6%
$

2.2 ‘go tool covdata merge’로 원시 프로파일 병합[ | ]

"-cover"로 빌드된 애플리케이션을 실행할 때마다 1개 이상의 데이터 파일이 GOCOVERDIR 환경변수에 지정된 디렉토리에 기록된다. 통합 테스트가 N개의 프로그램을 실행시키는 경우, 출력 디렉터리에는 O(N)개의 파일이 생긴다. 일반적으로 데이터 파일에는 중복된 콘텐츠가 많으므로 데이터를 압축하거나 다른 통합 테스트 실행의 데이터 세트를 결합하려면, go tool covdata merge 명령어를 사용하여 프로파일을 함께 병합할 수 있다.

예시:

$ /bin/sh wrap_test_for_coverage.sh
finished processing 380 files, no crashes
    gitlab.com/golang-commonmark/mdtool coverage: 54.6% of statements
$ ls covdatafiles
covcounters.13326b42c2a107249da22f6e0d35b638.772307.1677775306041466651
covcounters.13326b42c2a107249da22f6e0d35b638.772314.1677775306053066987
...
covcounters.13326b42c2a107249da22f6e0d35b638.774973.1677775310032569308
covmeta.13326b42c2a107249da22f6e0d35b638
$ ls covdatafiles | wc
    381     381   27401
$ rm -rf merged ; mkdir merged ; go tool covdata merge -i=covdatafiles -o=merged
$ ls merged
covcounters.13326b42c2a107249da22f6e0d35b638.0.1677775331350024014
covmeta.13326b42c2a107249da22f6e0d35b638
$

go tool covdata 병합 작업은, 필요시 특정 패키지 또는 패키지 집합을 선택하는 데 사용할 수 있는 pkg 플래그도 쓸 수 있다.

이 병합 기능은 다른 테스트 하네스에서 생성된 실행을 포함하여 다양한 유형의 테스트 실행 결과를 결합하는 데에도 유용하다.

3 마무리[ | ]

1.20 릴리스에서 Go의 커버리지 도구는 더 이상 패키지 테스트에 국한되지 않고 더 큰 통합 테스트에서 프로파일 수집을 지원한다. 더 크고 복잡한 테스트가 얼마나 잘 작동하는지, 소스 코드의 어느 부분이 실행되고 있는지 이해하는 데 도움이 되는 새로운 기능을 잘 활용해보자.

이러한 새로운 기능을 사용해 보고, 문제가 있면 언제나처럼 GitHub 이슈 트래커에 이슈를 등록하자. 고맙습니다.

4 같이 보기[ | ]

5 참고[ | ]

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