Slide 1

Slide 1 text

Who tests the Tests ? ~ Go Conference mini in Sendai 2026 ~ The Go gopher was designed by Renee French

Slide 2

Slide 2 text

もくじ ● なぜCIは重要であり、ぼくたちはテストを書くのか ● 嘘つきなカバレッジ ● Mutation Testing ● どのように Mutation Testingを実装するのか ● まとめ

Slide 3

Slide 3 text

自己紹介 渋谷拓真 Go Conference メインオーガナイザー OSS ○ Kubernetes メンテナー ○ Argo CD メンテナー Kubernetes 2025 Contributor Award 3Dプリンターにハマってます

Slide 4

Slide 4 text

なぜCIは重要であり、ぼくたちはテストを書くのか

Slide 5

Slide 5 text

CIは開発サイクルの最低限の約束

Slide 6

Slide 6 text

CIは開発サイクルの最低限の約束 CIの成功 = レビュー依頼の準備 ○ 「CI通ったのでレビューお願いします」 ○ 「CI失敗しているので直してください」 CIはチーム開発をする上での最低限の約束事として動いている ○ 「新規開発をするから GitHub Actionsから整えるぞ!」

Slide 7

Slide 7 text

CIは開発サイクルの最低限の約束

Slide 8

Slide 8 text

CIは開発サイクルの必須の約束 AI Agentが自己回帰する際のチェックポイントになる ○ 「CIが失敗したので調査をして継続します」 ○ 「CIが全てPASSしたので終了します」 出力するコードが指数関数的に増える中で自動で品質を担保することはより必 要な世界線で開発をしている

Slide 9

Slide 9 text

CIホスティングサービスでベースはすぐに整備できる name: Go on: [push] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-go@v5 with: go-version: 1.26 - run: go test ./...

Slide 10

Slide 10 text

コードの品質を担保するにはどのような手段があるか Linter / Analyzer ○ go vet ○ golangci-lint ○ go fix Test ○ go test

Slide 11

Slide 11 text

コードの品質を担保するにはどのような手段があるか Linter / Analyzer ○ go vet ○ golangci-lint ○ go fix Test ○ go test

Slide 12

Slide 12 text

なぜテストを書くのか テストはコードに対する実装の意図を明文化するための保証書 ○ テスト対象が何をやろうとしているか ○ テスト対象が何をやらないか ○ ホワイトボックステスト テストはコードに対しての一番近いクライアント ○ 実際のユーザーを擬似的に再現可能 ○ ブラックボックステスト

Slide 13

Slide 13 text

嘘つきなカバレッジ

Slide 14

Slide 14 text

テストによる計測 Goには様々なテスト手法がある ○ Test ○ Benchmark ○ Example ○ Fuzzing カバレッジをベースとしコードベース内で検証されている割合を計測する ○ go test -cover # unit test ○ go build -cover # e2e test

Slide 15

Slide 15 text

「ある指標が目標になると、その時点でその指標は “良い指標”ではなくなる」

Slide 16

Slide 16 text

数値だけを追い求めるケース タイピング速度が速くなると開発生産性が上がる → タイピング速度に固執する PRレビューの粒度は小さいとレビューしやすくていい → PRの数を貢献した指標としてしまうと分割することが目標となる テストのカバレッジを常に 80%維持する → 本質的に必要のないテストケースの増加へつながる

Slide 17

Slide 17 text

数値だけを追い求めるケース タイピング速度が速くなると開発生産性が上がる → タイピング速度に固執する PRレビューの粒度は小さいとレビューしやすくていい → PRの数を貢献した指標としてしまうと分割することが目標となる テストのカバレッジを常に 80%維持する → 本質的に必要のないテストケースの増加へつながる

Slide 18

Slide 18 text

100%のカバレッジ!! func Div(a, b int) (int, error) { if b == 0 { return 0, errors.New(“b must not be zero”) } return a / b, nil } func TestDiv(t *testing.T) { _, _ = Div(1, 1) _, _ = Div(1, 0) }

Slide 19

Slide 19 text

AI時代だと類似した出来事が起こりうることがある Xのテスト書いて わかりました t.Skipしてる!? ごめんなさい

Slide 20

Slide 20 text

Mutation Testing

Slide 21

Slide 21 text

Mutation Testingとは プログラムの一部を 意図的に書き換え 、生成したミュータントに対してテスト を実行し、失敗するかどうかを検証する ● 演算子の反転 : * → / , - → + , == → != , || → && ● 条件の変換 : elseの削除, caseの削除 ● ループの変換 : continue → break

Slide 22

Slide 22 text

Mutation Testingの評価方法 ミューテーションテストの実行後 ● テストが失敗 → ミュータントが KILL(成功) ● テストが成功 → ミュータントが SURVIVED(失敗) テストの指標は以下に多くのミュータントを KILLできたかで考える

Slide 23

Slide 23 text

Mutation Testingの注意事項 実行時間とのトレードオフ ● Mutation Testingではソースコードの修正が必須 ● 規模が大きいと実行時間も伸びるためどこに対して行うかを考える 100% KILLEDは目指さない ● 1 / 1 → 1 * 1 のように変換しても意味がない場合もある ● そのケースが必要かどうかを検討しながら時には Ignoreする 「ある指標が目標になると、その時点でその指標は “良い指標”ではなくなる」

Slide 24

Slide 24 text

どのように Mutation Testingを実装するのか

Slide 25

Slide 25 text

Mutation Testingの実装方法 sivchari/gomuの実装方法をベースとします GoのMutation Testingライブラリ overlayを活用しミューテーションテストを並列に実行する インクリメンタル解析を行い差分のみの Mutation Testingを実行可能 PRへのレポート生成と閾値以下のスコアで FailさせるCIファースト思想

Slide 26

Slide 26 text

Mutation Testingの挙動 パースして AST生成 ↓ Mutation生成 ↓ Mutationを適用して一時ファイルと overlay.jsonを生成 ↓ go test -overlay=overlay.json ./... を行いKILLED/SURVIVED判定

Slide 27

Slide 27 text

パースして AST生成 main.go → parser.ParseFile → AST 1 + 1 →

Slide 28

Slide 28 text

Mutation生成 ast.Inspect(fileAST, func(node ast.Node) bool { for _, mutator := range e.mutators { if mutator.CanMutate(node) { mutants = mutator.Mutate(node, fset) } } return true })

Slide 29

Slide 29 text

Mutation生成 ast.Inspect(fileAST, func(node ast.Node) bool { for _, mutator := range e.mutators { if mutator.CanMutate(node) { mutants = mutator.Mutate(node, fset) } } return true })

Slide 30

Slide 30 text

CanMutate switch node.(type) { case *ast.BinaryExpr: // a + b case *ast.AssignStmt: // a += b case *ast.IncDecStmt: // a++ return true } return false

Slide 31

Slide 31 text

Mutation生成 ast.Inspect(fileAST, func(node ast.Node) bool { for _, mutator := range e.mutators { if mutator.CanMutate(node) { mutants = mutator.Mutate(node, fset) } } return true })

Slide 32

Slide 32 text

Mutate switch n := node.(type) { case *ast.BinaryExpr: return m.mutateBinaryExpr(n, pos) }

Slide 33

Slide 33 text

Mutate switch n := node.(type) { case *ast.BinaryExpr: return m.mutateBinaryExpr(n, pos) }

Slide 34

Slide 34 text

mutateBinaryExpr mutations := m.getArithmeticMutations(expr.Op) mutants := make([]Mutant, 0, len(mutations)) for _, newOp := range mutations { mutants = append(mutants, Mutant{ Line: pos.Line, Column: pos.Column, Original: expr.Op.String(), // "+" Mutated: newOp.String(), // "-, *, /" }) } return mutants

Slide 35

Slide 35 text

mutateBinaryExpr mutations := m.getArithmeticMutations(expr.Op) mutants := make([]Mutant, 0, len(mutations)) for _, newOp := range mutations { mutants = append(mutants, Mutant{ Line: pos.Line, Column: pos.Column, Original: expr.Op.String(), // "+" Mutated: newOp.String(), // "-, *, /" }) } return mutants

Slide 36

Slide 36 text

getArithmeticMutations switch op { case token.ADD: // + // -, *, / return []token.Token{token.SUB, token.MUL, token.QUO} ... default: return nil }

Slide 37

Slide 37 text

Mutation適用とoverlya.jsonの生成 for i, mutant := range mutants { wg.Add(1) go func(index int, m mutation.Mutant) { defer wg.Done() result := e.runSingleMutation(m, timeout) }(i, mutant) }

Slide 38

Slide 38 text

Mutation適用とoverlya.jsonの生成 for i, mutant := range mutants { wg.Add(1) go func(index int, m mutation.Mutant) { defer wg.Done() result := e.runSingleMutation(m, timeout) }(i, mutant) }

Slide 39

Slide 39 text

mutation listをgoroutineで実行する

Slide 40

Slide 40 text

Mutation適用とoverlya.jsonの生成 for i, mutant := range mutants { wg.Add(1) go func(index int, m mutation.Mutant) { defer wg.Done() result := e.runSingleMutation(m, timeout) }(i, mutant) }

Slide 41

Slide 41 text

runSIngleMutation mutCtx, err := e.overlay.PrepareMutation(mutant) if err := e.checkCompilationWithOverlay(mutCtx); err != nil { result.Status = mutation.StatusNotViable result.Error = fmt.Sprintf("Compilation failed: %v", err) return result } return e.runTestWithOverlay(mutCtx, mutant, timeout)

Slide 42

Slide 42 text

overlayとは?? mutCtx, err := e.overlay.PrepareMutation(mutant) if err := e.checkCompilationWithOverlay(mutCtx); err != nil { result.Status = mutation.StatusNotViable result.Error = fmt.Sprintf("Compilation failed: %v", err) return result } return e.runTestWithOverlay(mutCtx, mutant, timeout)

Slide 43

Slide 43 text

overlayとは?? Go 1.16 で導入された機能で、実際のファイルを変更せずに、仮想的に別のファ イルで置き換える仕組み { "Replace": { "/path/to/main.go": "/tmp/mutant_1/main.go" } } go test -overlay=overlay.json ./...

Slide 44

Slide 44 text

なぜgomuでoverlay? 直接ファイルを修正すると途中で失敗すると戻せなくなる 失敗した場合に mutatedした後のソースファイルをデバッグとして残したい 実行時に置き換えるので goroutineで並列or並行に実行しても競合しない

Slide 45

Slide 45 text

KILLED/SURVIVED判定 mutCtx, err := e.overlay.PrepareMutation(mutant) if err := e.checkCompilationWithOverlay(mutCtx); err != nil { result.Status = mutation.StatusNotViable result.Error = fmt.Sprintf("Compilation failed: %v", err) return result } return e.runTestWithOverlay(mutCtx, mutant, timeout)

Slide 46

Slide 46 text

KILLED/SURVIVED判定 mutCtx, err := e.overlay.PrepareMutation(mutant) if err := e.checkCompilationWithOverlay(mutCtx); err != nil { result.Status = mutation.StatusNotViable result.Error = fmt.Sprintf("Compilation failed: %v", err) return result } return e.runTestWithOverlay(mutCtx, mutant, timeout)

Slide 47

Slide 47 text

runTestWithOverlay cmd := exec.CommandContext( ctx, "go", "test", "-overlay="+mutCtx.OverlayPath, "./..." ) cmd.Dir = testDir if err != nil { result.Status = mutation.StatusKilled } else { result.Status = mutation.StatusSurvived }

Slide 48

Slide 48 text

まとめ

Slide 49

Slide 49 text

まとめ ● AI時代、加速する環境の中、品質も加速して変わり続ける ● テストカバレッジは重要、同様に意味のあるテストかどうかも重要 ● Mutation Testingを活用してテストをテストしよう

Slide 50

Slide 50 text

ありがとうございました!