Updated:

15 minute read

개요

코드 커버리지는 테스트가 실제로 실행한 코드의 비율을 측정하여 테스트 품질을 평가하는 지표입니다.

주요 특징:

  • 내장 지원: 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:

[![codecov](https://codecov.io/gh/username/repo/branch/main/graph/badge.svg)](https://codecov.io/gh/username/repo)

Coveralls:

[![Coverage Status](https://coveralls.io/repos/github/username/repo/badge.svg?branch=main)](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, 시각화, 프로파일 병합
  • 원칙: 커버리지는 수단이지 목적이 아님