[Go] code coverage
Updated:
개요
코드 커버리지는 테스트가 실제로 실행한 코드의 비율을 측정하여 테스트 품질을 평가하는 지표입니다.
주요 특징:
- 내장 지원: Go 도구 체인에 기본 포함
- 다양한 모드: set, count, atomic
- 시각화: HTML 리포트 생성
- 패키지별 분석: 함수/라인별 상세 정보
- CI/CD 통합: 자동화된 품질 검증
- 커버리지 임계값: 최소 커버리지 강제
- 제외 패턴: 특정 코드 제외 가능
기본 사용법
1. 커버리지 측정
# 기본 커버리지 측정
go test -cover
# 결과:
# ok mypackage 0.003s coverage: 85.7% of statements
# 상세 출력
go test -cover -v
# 특정 패키지
go test -cover ./pkg/calculator
# 모든 하위 패키지
go test -cover ./...
2. 커버리지 프로파일 생성
# 프로파일 파일 생성
go test -coverprofile=coverage.out
# 특정 위치에 저장
go test -coverprofile=./coverage/coverage.out
# 여러 패키지 (병합됨)
go test -coverprofile=coverage.out ./...
3. HTML 리포트 생성
# HTML 리포트 생성 및 브라우저 열기
go tool cover -html=coverage.out
# HTML 파일로 저장
go tool cover -html=coverage.out -o coverage.html
# 브라우저에서 열기
open coverage.html # macOS
xdg-open coverage.html # Linux
start coverage.html # Windows
4. 함수별 커버리지 확인
# 함수별 커버리지 출력
go tool cover -func=coverage.out
# 결과 예시:
# mypackage/calculator.go:5: Add 100.0%
# mypackage/calculator.go:9: Subtract 100.0%
# mypackage/calculator.go:13: Multiply 80.0%
# mypackage/calculator.go:20: Divide 50.0%
# total: (statements) 82.5%
커버리지 모드
1. Set Mode (기본)
# 라인이 실행되었는지 여부만 기록
go test -covermode=set -coverprofile=coverage.out
# 특징:
# - 가장 빠름
# - 각 라인의 실행 여부만 추적 (0 또는 1)
# - 병렬 테스트에서 정확하지 않을 수 있음
프로파일 내용:
mode: set
mypackage/calculator.go:5.23,7.2 1 1
mypackage/calculator.go:9.28,11.2 1 1
mypackage/calculator.go:13.28,15.2 1 0
2. Count Mode
# 라인이 몇 번 실행되었는지 기록
go test -covermode=count -coverprofile=coverage.out
# 특징:
# - 실행 횟수 추적
# - 핫스팟 식별 가능
# - set보다 느림
# - 병렬 테스트에서 부정확할 수 있음
프로파일 내용:
mode: count
mypackage/calculator.go:5.23,7.2 1 3
mypackage/calculator.go:9.28,11.2 1 5
mypackage/calculator.go:13.28,15.2 1 0
3. Atomic Mode
# 동시성 안전한 카운트 모드
go test -covermode=atomic -coverprofile=coverage.out
# 특징:
# - count와 동일하지만 동시성 안전
# - 병렬 테스트에서 정확
# - 가장 느림
# - 멀티스레드 테스트에 필수
사용 시기:
func TestParallel(t *testing.T) {
tests := []struct {
name string
val int
}{
{"test1", 1},
{"test2", 2},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // 병렬 실행 시 atomic 모드 필요
// 테스트...
})
}
}
다중 패키지 커버리지
1. 전체 프로젝트 커버리지
# 모든 패키지 테스트 및 커버리지
go test -coverprofile=coverage.out ./...
# HTML 리포트
go tool cover -html=coverage.out
# 함수별 리포트
go tool cover -func=coverage.out
2. 특정 패키지만 커버리지 측정
# 단일 패키지
go test -coverprofile=coverage.out ./pkg/calculator
# 여러 특정 패키지
go test -coverprofile=coverage.out ./pkg/calculator ./pkg/parser
# 패턴 매칭
go test -coverprofile=coverage.out ./pkg/...
3. coverpkg 옵션
# 특정 패키지의 커버리지만 측정
go test -coverpkg=./pkg/calculator -coverprofile=coverage.out ./...
# 여러 패키지
go test -coverpkg=./pkg/calculator,./pkg/parser -coverprofile=coverage.out ./...
# 모든 패키지 (통합 테스트에 유용)
go test -coverpkg=./... -coverprofile=coverage.out ./...
예시: 통합 테스트 커버리지
// pkg/api/handler.go
package api
import "myapp/pkg/calculator"
func HandleAdd(a, b int) int {
return calculator.Add(a, b) // calculator 패키지 코드 실행
}
// pkg/api/handler_test.go
func TestHandleAdd(t *testing.T) {
result := HandleAdd(2, 3)
if result != 5 {
t.Fail()
}
}
# api 테스트만 실행하지만 calculator 커버리지도 측정
go test -coverpkg=./pkg/calculator,./pkg/api \
-coverprofile=coverage.out ./pkg/api
커버리지 분석
1. 커버리지 리포트 읽기
go tool cover -func=coverage.out
출력 예시:
myapp/pkg/calculator/calculator.go:5: Add 100.0%
myapp/pkg/calculator/calculator.go:9: Subtract 100.0%
myapp/pkg/calculator/calculator.go:13: Multiply 75.0%
myapp/pkg/calculator/calculator.go:20: Divide 60.0%
myapp/pkg/calculator/calculator.go:30: Power 0.0%
myapp/pkg/parser/parser.go:10: Parse 85.0%
myapp/pkg/parser/parser.go:45: Validate 50.0%
total: (statements) 74.2%
분석:
Power함수: 테스트 없음 (0%)Validate함수: 테스트 부족 (50%)- 전체: 74.2% (목표: 80%)
2. HTML 리포트 분석
go tool cover -html=coverage.out
색상 코드:
- 초록색: 실행된 코드 (커버됨)
- 빨간색: 실행되지 않은 코드 (커버되지 않음)
- 회색: 추적되지 않는 코드 (주석, 선언문 등)
활용:
- 빨간색 영역 → 테스트 추가 필요
- 에러 핸들링 누락 확인
- 엣지 케이스 테스트 부족 확인
3. 커버리지 프로파일 구조
mode: set
myapp/calculator.go:5.23,7.2 1 1
myapp/calculator.go:9.28,11.2 1 1
myapp/calculator.go:13.28,17.16 3 1
myapp/calculator.go:20.2,20.12 1 1
myapp/calculator.go:17.16,19.3 1 0
형식:
파일명:시작라인.시작열,끝라인.끝열 문장수 실행횟수
예시 해석:
myapp/calculator.go:13.28,17.16 3 1
- 파일:
myapp/calculator.go - 범위: 13라인 28열 ~ 17라인 16열
- 문장 수: 3개
- 실행 횟수: 1번
실전 예제
1. 기본 커버리지 개선
초기 코드:
// calculator.go
package calculator
import "errors"
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func IsEven(n int) bool {
if n%2 == 0 {
return true
}
return false
}
func Max(a, b int) int {
if a > b {
return a
}
return b
}
초기 테스트 (커버리지 33%):
// calculator_test.go
package calculator
import "testing"
func TestDivide(t *testing.T) {
result, err := Divide(10, 2)
if err != nil {
t.Fatal(err)
}
if result != 5 {
t.Errorf("got %d, want 5", result)
}
}
$ go test -cover
coverage: 33.3% of statements
개선된 테스트 (커버리지 100%):
func TestDivide(t *testing.T) {
tests := []struct {
name string
a, b int
want int
wantErr bool
}{
{"valid", 10, 2, 5, false},
{"divide by zero", 10, 0, 0, true},
{"negative", -10, 2, -5, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Divide(tt.a, tt.b)
if (err != nil) != tt.wantErr {
t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
}
if got != tt.want {
t.Errorf("got %d, want %d", got, tt.want)
}
})
}
}
func TestIsEven(t *testing.T) {
tests := []struct {
n int
want bool
}{
{2, true},
{3, false},
{0, true},
{-2, true},
{-3, false},
}
for _, tt := range tests {
got := IsEven(tt.n)
if got != tt.want {
t.Errorf("IsEven(%d) = %v, want %v", tt.n, got, tt.want)
}
}
}
func TestMax(t *testing.T) {
tests := []struct {
a, b int
want int
}{
{5, 3, 5},
{3, 5, 5},
{5, 5, 5},
}
for _, tt := range tests {
got := Max(tt.a, tt.b)
if got != tt.want {
t.Errorf("Max(%d, %d) = %d, want %d",
tt.a, tt.b, got, tt.want)
}
}
}
$ go test -cover
coverage: 100.0% of statements
2. 에러 핸들링 커버리지
// file.go
package fileutil
import (
"io"
"os"
)
func ReadFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err // 커버리지 필요
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return nil, err // 커버리지 필요
}
return data, nil
}
테스트:
// file_test.go
func TestReadFile(t *testing.T) {
t.Run("success", func(t *testing.T) {
// 임시 파일 생성
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "test.txt")
content := []byte("hello")
err := os.WriteFile(tmpFile, content, 0644)
require.NoError(t, err)
// 읽기 테스트
data, err := ReadFile(tmpFile)
require.NoError(t, err)
assert.Equal(t, content, data)
})
t.Run("file not found", func(t *testing.T) {
_, err := ReadFile("nonexistent.txt")
assert.Error(t, err) // 에러 경로 커버
})
// io.ReadAll 에러는 실제 상황에서 발생하기 어려움
// 통합 테스트나 모킹 필요
}
3. 조건문 커버리지
// validator.go
func ValidateAge(age int) error {
if age < 0 {
return errors.New("age cannot be negative")
}
if age < 18 {
return errors.New("must be 18 or older")
}
if age > 150 {
return errors.New("age is unrealistic")
}
return nil
}
완전한 커버리지 테스트:
func TestValidateAge(t *testing.T) {
tests := []struct {
age int
wantErr bool
errMsg string
}{
{-1, true, "age cannot be negative"}, // 첫 번째 조건
{10, true, "must be 18 or older"}, // 두 번째 조건
{200, true, "age is unrealistic"}, // 세 번째 조건
{25, false, ""}, // 정상 경로
{18, false, ""}, // 경계값
{150, false, ""}, // 경계값
}
for _, tt := range tests {
err := ValidateAge(tt.age)
if (err != nil) != tt.wantErr {
t.Errorf("age %d: got error %v, wantErr %v",
tt.age, err, tt.wantErr)
}
if err != nil && err.Error() != tt.errMsg {
t.Errorf("age %d: got %q, want %q",
tt.age, err.Error(), tt.errMsg)
}
}
}
CI/CD 통합
1. GitHub Actions
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Run tests with coverage
run: go test -coverprofile=coverage.out ./...
- name: Generate coverage report
run: go tool cover -html=coverage.out -o coverage.html
- name: Check coverage threshold
run: |
coverage=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
threshold=80
if (( $(echo "$coverage < $threshold" | bc -l) )); then
echo "Coverage $coverage% is below threshold $threshold%"
exit 1
fi
echo "Coverage $coverage% meets threshold"
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage.out
flags: unittests
2. GitLab CI
# .gitlab-ci.yml
test:
image: golang:1.21
stage: test
script:
- go test -coverprofile=coverage.out ./...
- go tool cover -func=coverage.out
- go tool cover -html=coverage.out -o coverage.html
coverage: '/total:\s+\(statements\)\s+(\d+\.\d+)%/'
artifacts:
paths:
- coverage.html
- coverage.out
reports:
coverage_report:
coverage_format: cobertura
path: coverage.out
3. Makefile
# Makefile
.PHONY: test coverage coverage-html coverage-check
test:
go test -v ./...
coverage:
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
coverage-html:
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
@echo "Opening coverage report..."
open coverage.html # macOS
# xdg-open coverage.html # Linux
coverage-check:
@go test -coverprofile=coverage.out ./... > /dev/null
@coverage=$$(go tool cover -func=coverage.out | grep total | awk '{print $$3}' | sed 's/%//'); \
threshold=80; \
if [ $$(echo "$$coverage < $$threshold" | bc) -eq 1 ]; then \
echo "❌ Coverage $$coverage% is below threshold $$threshold%"; \
exit 1; \
else \
echo "✅ Coverage $$coverage% meets threshold $$threshold%"; \
fi
# 사용:
# make coverage # 커버리지 측정 및 출력
# make coverage-html # HTML 리포트 생성
# make coverage-check # 임계값 검증
4. 커버리지 배지
Codecov:
[](https://codecov.io/gh/username/repo)
Coveralls:
[](https://coveralls.io/github/username/repo?branch=main)
커버리지 최적화
1. 테스트 불가능한 코드 제외
// main.go
package main
import (
"fmt"
"os"
"myapp/pkg/app"
)
func main() {
if err := run(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
func run() error {
// 테스트 가능한 로직
return app.Start()
}
run() 함수만 테스트:
// main_test.go
func TestRun(t *testing.T) {
err := run()
assert.NoError(t, err)
}
2. 빌드 태그로 분리
//go:build !test
// +build !test
package database
// 실제 데이터베이스 연결 (테스트에서 제외)
func Connect() (*DB, error) {
// ...
}
//go:build test
// +build test
package database
// 테스트용 모킹
func Connect() (*DB, error) {
return NewMockDB(), nil
}
3. 인터페이스로 테스트 용이성 향상
개선 전:
type UserService struct {}
func (s *UserService) GetUser(id int) (*User, error) {
// 직접 데이터베이스 접근 (테스트 어려움)
db, err := sql.Open("postgres", "...")
if err != nil {
return nil, err
}
// ...
}
개선 후:
type UserRepository interface {
GetUser(id int) (*User, error)
}
type UserService struct {
repo UserRepository
}
func (s *UserService) GetUser(id int) (*User, error) {
return s.repo.GetUser(id) // 인터페이스 사용
}
// 테스트
type mockUserRepository struct {
users map[int]*User
}
func (m *mockUserRepository) GetUser(id int) (*User, error) {
user, ok := m.users[id]
if !ok {
return nil, errors.New("not found")
}
return user, nil
}
func TestUserService_GetUser(t *testing.T) {
repo := &mockUserRepository{
users: map[int]*User{
1: {ID: 1, Name: "John"},
},
}
service := &UserService{repo: repo}
user, err := service.GetUser(1)
assert.NoError(t, err)
assert.Equal(t, "John", user.Name)
}
4. 커버리지 갭 분석 스크립트
#!/bin/bash
# coverage_gap.sh
# 커버리지 생성
go test -coverprofile=coverage.out ./...
# 함수별 커버리지 출력
echo "=== Low Coverage Functions (< 80%) ==="
go tool cover -func=coverage.out | awk '$3 != "total:" && $NF+0 < 80 { print $0 }'
# 파일별 커버리지
echo ""
echo "=== Coverage by File ==="
go tool cover -func=coverage.out | grep -v "total:" | \
awk '{file=$1; coverage=$3; sum[file]+=coverage; count[file]++}
END {for (f in sum) printf "%s\t%.1f%%\n", f, sum[f]/count[f]}' | \
sort -t $'\t' -k2 -n
# 전체 커버리지
echo ""
echo "=== Total Coverage ==="
go tool cover -func=coverage.out | grep total
커버리지 메트릭
1. 커버리지 타입
Statement Coverage (문장 커버리지):
// Go가 측정하는 기본 커버리지
func Max(a, b int) int {
if a > b { // 문장 1
return a // 문장 2
}
return b // 문장 3
}
// 100% 커버리지: 모든 문장 실행
TestMax(5, 3) // 문장 1, 2 실행
TestMax(3, 5) // 문장 1, 3 실행
Branch Coverage (분기 커버리지):
// Go는 직접 측정하지 않지만 암묵적으로 포함
func Validate(age int, hasID bool) bool {
if age >= 18 && hasID { // 4가지 조합 가능
return true
}
return false
}
// 완전한 분기 커버리지:
TestValidate(18, true) // true && true
TestValidate(18, false) // true && false
TestValidate(17, true) // false && true
TestValidate(17, false) // false && false
2. 커버리지 목표
일반적인 권장사항:
- 최소: 70% (허용 가능한 하한선)
- 권장: 80% (좋은 품질)
- 이상적: 90%+ (매우 높은 품질)
- 100%: 현실적이지 않음 (비용 대비 효과 낮음)
프로젝트별 조정:
// 크리티컬 패키지: 95%+
package payment // 결제 로직
// 비즈니스 로직: 85%+
package service
// 유틸리티: 80%+
package util
// 생성된 코드: 제외
package generated // protobuf, 등
3. 커버리지 트렌드 추적
# .github/workflows/coverage-trend.yml
name: Coverage Trend
on: [push]
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # 전체 히스토리
- name: Set up Go
uses: actions/setup-go@v4
- name: Generate coverage
run: go test -coverprofile=coverage.out ./...
- name: Extract coverage percentage
id: coverage
run: |
coverage=$(go tool cover -func=coverage.out | grep total | awk '{print $3}')
echo "percentage=$coverage" >> $GITHUB_OUTPUT
- name: Update coverage badge
run: |
# README.md에 커버리지 업데이트
echo "Coverage: ${{ steps.coverage.outputs.percentage }}"
- name: Comment on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v6
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '📊 Code Coverage: ${{ steps.coverage.outputs.percentage }}'
})
일반적인 실수
1. 100% 커버리지 집착
// ❌ 100% 커버리지를 위한 의미 없는 테스트
func TestGetter(t *testing.T) {
u := &User{Name: "John"}
if u.Name != "John" { // 단순 getter 테스트
t.Fail()
}
}
// ✅ 의미 있는 비즈니스 로직 테스트에 집중
func TestUserValidation(t *testing.T) {
u := &User{Name: "", Email: "invalid"}
err := u.Validate()
assert.Error(t, err)
}
2. 커버리지 != 품질
// ❌ 높은 커버리지, 낮은 품질
func TestDivide(t *testing.T) {
Divide(10, 2) // 결과 검증 없음
Divide(10, 0) // 에러 검증 없음
}
// 100% 커버리지지만 아무것도 검증하지 않음
// ✅ 적절한 검증
func TestDivide(t *testing.T) {
result, err := Divide(10, 2)
assert.NoError(t, err)
assert.Equal(t, 5, result)
_, err = Divide(10, 0)
assert.Error(t, err)
}
3. 생성 코드 포함
// ❌ protobuf 생성 코드도 커버리지에 포함
go test -coverprofile=coverage.out ./...
// ✅ 생성 코드 제외
go test -coverprofile=coverage.out $(go list ./... | grep -v /generated/)
4. 통합 테스트 커버리지 누락
# ❌ 유닛 테스트만 측정
go test -coverprofile=coverage.out ./...
# ✅ 통합 테스트도 포함
go test -tags=integration -coverpkg=./... -coverprofile=coverage.out ./...
5. 병렬 테스트에서 잘못된 모드
func TestParallel(t *testing.T) {
t.Parallel() // 병렬 실행
// ...
}
# ❌ set 모드 (부정확할 수 있음)
go test -covermode=set -coverprofile=coverage.out
# ✅ atomic 모드 (병렬 안전)
go test -covermode=atomic -coverprofile=coverage.out
6. 커버리지 파일 덮어쓰기
# ❌ 패키지별로 실행하면 이전 커버리지 손실
go test -coverprofile=coverage.out ./pkg/a
go test -coverprofile=coverage.out ./pkg/b # pkg/a 커버리지 손실
# ✅ 한 번에 모든 패키지
go test -coverprofile=coverage.out ./...
# 또는 병합
go test -coverprofile=coverage_a.out ./pkg/a
go test -coverprofile=coverage_b.out ./pkg/b
# gocovmerge 도구로 병합
7. 중요도 무시
// ❌ 모든 코드를 동등하게 취급
// main.go의 init 함수: 80% 커버리지
// payment.go의 결제 로직: 80% 커버리지
// ✅ 크리티컬 코드는 더 높은 기준 적용
// main.go: 70% 이상
// payment.go: 95% 이상
베스트 프랙티스
1. 점진적 개선
# 현재 커버리지 확인
go test -cover ./...
# 낮은 커버리지 함수 식별
go tool cover -func=coverage.out | awk '$NF+0 < 80'
# 우선순위 설정
# 1. 크리티컬 비즈니스 로직
# 2. 자주 변경되는 코드
# 3. 버그가 많이 발생한 코드
# 점진적으로 테스트 추가
2. 커버리지 게이트
# .github/workflows/test.yml
- name: Coverage gate
run: |
# PR은 기존보다 낮아지면 안됨
current=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
baseline=80
if (( $(echo "$current < $baseline" | bc -l) )); then
echo "❌ Coverage $current% < $baseline%"
exit 1
fi
echo "✅ Coverage $current%"
3. 커버리지 리포트 자동화
#!/bin/bash
# generate_coverage_report.sh
set -e
echo "🧪 Running tests with coverage..."
go test -coverprofile=coverage.out -covermode=atomic ./...
echo ""
echo "📊 Coverage Summary:"
go tool cover -func=coverage.out | grep total
echo ""
echo "📈 Low Coverage Files (<80%):"
go tool cover -func=coverage.out | \
awk '$1 !~ /total/ && $NF+0 < 80 {print $1, $NF}'
echo ""
echo "🌐 Generating HTML report..."
go tool cover -html=coverage.out -o coverage.html
echo ""
echo "✅ Done! Open coverage.html to view detailed report."
4. 패키지별 커버리지 목표
// coverage_goals.yaml
packages:
- path: "./pkg/payment"
minimum: 95
reason: "Critical payment logic"
- path: "./pkg/api"
minimum: 85
reason: "Public API"
- path: "./pkg/util"
minimum: 75
reason: "Utility functions"
- path: "./cmd"
minimum: 50
reason: "CLI entry points"
5. 리뷰 체크리스트
PR 리뷰 시 확인:
- 전체 커버리지가 유지되거나 향상되었는가?
- 새로운 코드에 대한 테스트가 있는가?
- 크리티컬 경로가 모두 테스트되었는가?
- 에러 케이스가 테스트되었는가?
- 경계값 테스트가 포함되었는가?
고급 기법
1. Differential Coverage
#!/bin/bash
# diff_coverage.sh - 변경된 코드의 커버리지만 측정
# 현재 브랜치 커버리지
go test -coverprofile=coverage_new.out ./...
# main 브랜치로 전환
git checkout main
go test -coverprofile=coverage_base.out ./...
git checkout -
# 차이 분석
echo "Changed files coverage:"
git diff main --name-only | grep '\.go$' | grep -v '_test\.go$' | while read file; do
if [ -f "$file" ]; then
coverage=$(go tool cover -func=coverage_new.out | grep "$file" | awk '{sum+=$3; count++} END {print sum/count}')
echo "$file: $coverage%"
fi
done
2. 커버리지 시각화
// coverage_viz.go
package main
import (
"fmt"
"os/exec"
"regexp"
"sort"
)
type FileCoverage struct {
File string
Coverage float64
}
func main() {
cmd := exec.Command("go", "tool", "cover", "-func=coverage.out")
output, _ := cmd.Output()
re := regexp.MustCompile(`([^\s]+\.go):\d+:\s+\S+\s+([\d.]+)%`)
matches := re.FindAllStringSubmatch(string(output), -1)
coverageMap := make(map[string][]float64)
for _, match := range matches {
file := match[1]
coverage := parseFloat(match[2])
coverageMap[file] = append(coverageMap[file], coverage)
}
var results []FileCoverage
for file, coverages := range coverageMap {
avg := average(coverages)
results = append(results, FileCoverage{file, avg})
}
sort.Slice(results, func(i, j int) bool {
return results[i].Coverage < results[j].Coverage
})
// ASCII 바 차트 출력
for _, fc := range results {
bar := generateBar(fc.Coverage)
fmt.Printf("%-40s %s %.1f%%\n", fc.File, bar, fc.Coverage)
}
}
func generateBar(coverage float64) string {
width := int(coverage / 5) // 0-20 characters
bar := ""
for i := 0; i < width; i++ {
bar += "█"
}
return bar
}
3. 커버리지 프로파일 병합
# 여러 프로파일 병합
go install github.com/wadey/gocovmerge@latest
go test -coverprofile=coverage1.out ./pkg/a
go test -coverprofile=coverage2.out ./pkg/b
go test -coverprofile=coverage3.out ./integration/...
gocovmerge coverage1.out coverage2.out coverage3.out > coverage.out
go tool cover -html=coverage.out
정리
- 기본 사용:
-coverprofile,-covermode, HTML 리포트 - 모드: set (빠름), count (실행 횟수), atomic (병렬 안전)
- 분석: 함수별, 파일별, 전체 커버리지
- 다중 패키지:
./...,-coverpkg옵션 - CI/CD: GitHub Actions, GitLab CI, 자동화
- 목표: 80% 권장, 크리티컬 코드는 더 높게
- 실수: 100% 집착, 품질 무시, 생성 코드 포함
- 베스트: 점진적 개선, 게이트 설정, 패키지별 목표
- 고급: Differential coverage, 시각화, 프로파일 병합
- 원칙: 커버리지는 수단이지 목적이 아님