Go言語入門:統合テストのカバレッジ -Vol.12-

スポンサーリンク
Go言語入門:統合テストのカバレッジ -Vol.12- ノウハウ
Go言語入門:統合テストのカバレッジ -Vol.12-
この記事は約12分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、Go言語の.統合テストのカバレッジついて解説しています。

スポンサーリンク

背景

Go言語でテストを書いていると、「go test -coverprofileでカバレッジが取れるのは知っているけど、統合テストのカバレッジってどうやって測るんだろう?」と疑問に思ったことはありませんか?

公式ドキュメントには、Go 1.20から統合テストのカバレッジ測定がサポートされたことが書かれていますが、英語で書かれている上に、ユニットテストとの違いや具体的な手順の説明が簡潔すぎて、初めて読むと「結局どうすればいいの?」と戸惑ってしまうかもしれません。

この記事では、公式ドキュメントの内容を丁寧な日本語に翻訳し、さらに初心者の方でも理解できるように、ユニットテストと統合テストの違い、なぜ3ステップ必要なのか、そして実際にどのようなコマンドを実行すればよいのかを、具体例を交えて解説していきます。

統合テストのカバレッジ測定は一見難しそうに見えますが、仕組みを理解すれば決して複雑ではありません。実際のアプリケーションでどれだけコードがテストされているかを把握することで、より品質の高いソフトウェア開発ができるようになります。一緒に学んでいきましょう!

-coverpkg=mainで自分のmainパッケージがプロファイリング対象として選択されますか?

-coverpkgフラグは、パッケージ名のリストではなく、インポートパスのリストを受け入れます。カバレッジインストルメンテーション用にmainパッケージを選択したい場合は、名前ではなくインポートパスで識別してください。例(この例のプログラムを使用):

$ 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

解説

パッケージ名とインポートパスの違い

よくある間違い

// main.go
package main  // ← これは「パッケージ名」

import "fmt"

func main() {
    fmt.Println("Hello")
}

多くの初心者が以下のように考えます:

# ❌ 間違い: パッケージ名で指定
go build -coverpkg=main -o myapp .

結果:

warning: no packages being built depend on matches for pattern main
# 警告が出て、カバレッジが測定されない

パッケージ名 vs インポートパス

パッケージ名

package main  // ← これがパッケージ名
package handlers
package models

特徴:

  • ソースコード内でpackageキーワードの後に書くもの
  • 同じディレクトリ内のすべての.goファイルで同じ名前を使う
  • これは-coverpkgで使えない

インポートパス

mydomain.com                    ← これがインポートパス
mydomain.com/handlers
mydomain.com/models
github.com/user/project

特徴:

  • go.modのmodule名から始まる完全パス
  • 他のパッケージからimportする時に使うパス
  • これを-coverpkgで使う

プロジェクトのインポートパスを確認する方法

方法1: go list -m

$ go list -m
mydomain.com

これがプロジェクトのモジュールパス(ベースインポートパス)です。

方法2: go.modファイルを確認

$ cat go.mod
module mydomain.com

go 1.20

最初の行のmoduleの後がモジュールパス=ルートのインポートパスです。

方法3: プロジェクト構造から推測

myproject/
├── go.mod (module github.com/user/myproject)
├── main.go                    → github.com/user/myproject
├── handlers/
│   └── api.go                → github.com/user/myproject/handlers
└── models/
    └── user.go               → github.com/user/myproject/models

正しい指定方法

例1: シンプルなプロジェクト

# プロジェクト構造
myproject/
├── go.mod (module mydomain.com)
└── main.go (package main)

# インポートパスを確認
$ go list -m
mydomain.com

# ❌ 間違い: パッケージ名で指定
$ go build -coverpkg=main -o myapp .
warning: no packages being built depend on matches for pattern main

# ✅ 正解: インポートパスで指定
$ go build -coverpkg=mydomain.com -o myapp .

例2: GitHubプロジェクト

# プロジェクト構造
myproject/
├── go.mod (module github.com/user/myproject)
└── main.go (package main)

# インポートパスを確認
$ go list -m
github.com/user/myproject

# ❌ 間違い
$ go build -coverpkg=main -o myapp .

# ✅ 正解
$ go build -coverpkg=github.com/user/myproject -o myapp .

例3: サブパッケージを含むプロジェクト

# プロジェクト構造
myproject/
├── go.mod (module example.com/myapp)
├── main.go (package main)
├── handlers/
│   └── api.go (package handlers)
└── models/
    └── user.go (package models)

# ルートパッケージのみ
$ go build -coverpkg=example.com/myapp -o myapp .

# すべてのサブパッケージを含む
$ go build -coverpkg=example.com/myapp/... -o myapp .

実際のエラーケース

エラー例1: パッケージ名を使用

$ cat go.mod
module mywebapp

$ cat main.go
package main

import "fmt"

func main() {
    fmt.Println("Hello")
}

# ❌ パッケージ名で指定
$ go build -coverpkg=main -o webapp .
warning: no packages being built depend on matches for pattern main

# 実行してもカバレッジが記録されない
$ mkdir coverage
$ GOCOVERDIR=coverage ./webapp
Hello

$ go tool covdata percent -i=coverage
# 何も表示されない

修正版

# ✅ インポートパスで指定
$ go build -coverpkg=mywebapp -o webapp .

$ GOCOVERDIR=coverage ./webapp
Hello

$ go tool covdata percent -i=coverage
    mywebapp    coverage: 100.0% of statements

なぜこの設計なのか

理由1: パッケージ名は重複する可能性がある

プロジェクト構造:
├── go.mod (module myapp)
├── main.go (package main)          ← mainという名前
├── cmd/
│   └── tool/
│       └── main.go (package main)  ← これもmain!
└── internal/
    └── main/
        └── core.go (package main)  ← これもmain!!

3つすべてpackage mainですが、インポートパスは異なります:

  • myapp
  • myapp/cmd/tool
  • myapp/internal/main

理由2: インポートパスは一意

インポートパスは常にプロジェクト内で一意なので、正確にパッケージを特定できます。

パッケージ名とインポートパスの対応表

ディレクトリpackage宣言インポートパス(-coverpkgで使用)
/package mainmyapp
/handlerspackage handlersmyapp/handlers
/modelspackage modelsmyapp/models
/cmd/toolpackage mainmyapp/cmd/tool
/internal/utilspackage utilsmyapp/internal/utils

実践スクリプト例

#!/bin/bash
# build-with-correct-coverpkg.sh

# モジュールパスを自動取得
MODULE_PATH=$(go list -m)

echo "モジュールパス: $MODULE_PATH"

# すべてのサブパッケージを含める
echo "ビルド中..."
go build -cover -coverpkg=$MODULE_PATH/... -o myapp .

# 実行
echo "テスト実行中..."
rm -rf coverage
mkdir coverage
GOCOVERDIR=coverage ./myapp

# レポート
echo ""
echo "=== カバレッジレポート ==="
go tool covdata percent -i=coverage

echo ""
echo "完了!"

より高度な使い方

パターン1: 複数のモジュール

# 複数のプロジェクトが同じリポジトリにある場合
myrepo/
├── serviceA/
│   ├── go.mod (module github.com/user/serviceA)
│   └── main.go
└── serviceB/
    ├── go.mod (module github.com/user/serviceB)
    └── main.go

# serviceAをビルド
cd serviceA
go build -coverpkg=github.com/user/serviceA/... -o serviceA .

# serviceBをビルド
cd ../serviceB
go build -coverpkg=github.com/user/serviceB/... -o serviceB .

パターン2: ベンダリングを使用

# vendor/ ディレクトリを使っている場合
myproject/
├── go.mod (module myapp)
├── vendor/
│   └── github.com/external/lib/
└── main.go

# 自分のコードのみ測定(vendorは除外)
go build -coverpkg=myapp/... -o myapp .

トラブルシューティング

問題1: 警告が出る

$ go build -coverpkg=main -o myapp .
warning: no packages being built depend on matches for pattern main

解決策:

# インポートパスを確認
go list -m

# 正しいインポートパスを使用
go build -coverpkg=$(go list -m) -o myapp .

問題2: カバレッジが0%

$ go tool covdata percent -i=coverage
# 何も表示されない、または0%

原因: -coverpkgで指定したパスが間違っている

確認方法:

# ビルド時の警告を確認
go build -coverpkg=wrong/path -o myapp .
# warning が出ていないか確認

# または、デフォルト(指定なし)で試す
go build -cover -o myapp .
GOCOVERDIR=coverage ./myapp
go tool covdata percent -i=coverage

問題3: どのインポートパスを使えばいいか分からない

# 利用可能なすべてのパッケージをリスト
go list ./...

# 出力例:
# myapp
# myapp/handlers
# myapp/models
# myapp/utils

# これらがインポートパスなので、-coverpkgで使える
go build -coverpkg=myapp,myapp/handlers,myapp/models -o myapp .

# または全部含める
go build -coverpkg=myapp/... -o myapp .

ヘルパー関数

# .bashrcや.zshrcに追加できるヘルパー関数

# 現在のモジュールパスでカバレッジビルド
gobuild-cov() {
    local module_path=$(go list -m)
    echo "Building with coverage for: $module_path/..."
    go build -cover -coverpkg=$module_path/... -o "${1:-app}" .
}

# 使用例:
# $ gobuild-cov myapp
# Building with coverage for: mydomain.com/...
# $ GOCOVERDIR=coverage ./myapp

まとめ

項目説明
パッケージ名package宣言で使う名前main, handlers
インポートパスモジュールパス+ディレクトリパスmyapp, myapp/handlers
-coverpkgで使用インポートパス(パッケージ名ではない)-coverpkg=myapp
確認コマンドgo list -mモジュールパスを表示
全パッケージ指定モジュールパス/...-coverpkg=myapp/...

重要なポイント:

  • -coverpkg=main は動かない(パッケージ名)
  • -coverpkg=mydomain.com が正しい(インポートパス)
  • go list -m でモジュールパスを確認
  • go.modmodule 行を見る
  • インポートパスは一意、パッケージ名は重複する可能性がある

クイックリファレンス:

# インポートパスを確認
go list -m

# そのパスを-coverpkgで使用
go build -cover -coverpkg=$(go list -m)/... -o myapp .

# または手動で
go build -cover -coverpkg=mydomain.com/... -o myapp .

おわりに 

本日は、Go言語の統合テストのカバレッジについて解説しました。

よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました