Share
Embed
Who tests the Tests ?
sivchari
February 26, 2026
Transcript
50 slides
-
Who tests the Tests ? ~ Go Conference mini in
Sendai 2026 ~ The Go gopher was designed by Renee French -
もくじ • なぜCIは重要であり、ぼくたちはテストを書くのか • 嘘つきなカバレッジ • Mutation Testing • どのように
Mutation Testingを実装するのか • まとめ -
自己紹介 渋谷拓真 Go Conference メインオーガナイザー OSS ◦ Kubernetes メンテナー ◦
Argo CD メンテナー Kubernetes 2025 Contributor Award 3Dプリンターにハマってます -
CIは開発サイクルの最低限の約束 CIの成功 = レビュー依頼の準備 ◦ 「CI通ったのでレビューお願いします」 ◦ 「CI失敗しているので直してください」 CIはチーム開発をする上での最低限の約束事として動いている ◦
「新規開発をするから GitHub Actionsから整えるぞ!」 -
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 ./... -
コードの品質を担保するにはどのような手段があるか Linter / Analyzer ◦ go vet ◦ golangci-lint ◦
go fix Test ◦ go test -
コードの品質を担保するにはどのような手段があるか Linter / Analyzer ◦ go vet ◦ golangci-lint ◦
go fix Test ◦ go test -
なぜテストを書くのか テストはコードに対する実装の意図を明文化するための保証書 ◦ テスト対象が何をやろうとしているか ◦ テスト対象が何をやらないか ◦ ホワイトボックステスト テストはコードに対しての一番近いクライアント ◦
実際のユーザーを擬似的に再現可能 ◦ ブラックボックステスト -
テストによる計測 Goには様々なテスト手法がある ◦ Test ◦ Benchmark ◦ Example ◦ Fuzzing
カバレッジをベースとしコードベース内で検証されている割合を計測する ◦ go test -cover # unit test ◦ go build -cover # e2e test -
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) } -
Mutation Testingとは プログラムの一部を 意図的に書き換え 、生成したミュータントに対してテスト を実行し、失敗するかどうかを検証する • 演算子の反転 : *
→ / , - → + , == → != , || → && • 条件の変換 : elseの削除, caseの削除 • ループの変換 : continue → break -
Mutation Testingの評価方法 ミューテーションテストの実行後 • テストが失敗 → ミュータントが KILL(成功) • テストが成功
→ ミュータントが SURVIVED(失敗) テストの指標は以下に多くのミュータントを KILLできたかで考える -
Mutation Testingの注意事項 実行時間とのトレードオフ • Mutation Testingではソースコードの修正が必須 • 規模が大きいと実行時間も伸びるためどこに対して行うかを考える 100% KILLEDは目指さない
• 1 / 1 → 1 * 1 のように変換しても意味がない場合もある • そのケースが必要かどうかを検討しながら時には Ignoreする 「ある指標が目標になると、その時点でその指標は “良い指標”ではなくなる」 -
Mutation Testingの挙動 パースして AST生成 ↓ Mutation生成 ↓ Mutationを適用して一時ファイルと overlay.jsonを生成 ↓
go test -overlay=overlay.json ./... を行いKILLED/SURVIVED判定 -
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 }) -
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 }) -
CanMutate switch node.(type) { case *ast.BinaryExpr: // a + b
case *ast.AssignStmt: // a += b case *ast.IncDecStmt: // a++ return true } return false -
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 }) -
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 -
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 -
getArithmeticMutations switch op { case token.ADD: // + // -,
*, / return []token.Token{token.SUB, token.MUL, token.QUO} ... default: return nil } -
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) } -
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) } -
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) } -
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) -
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) -
overlayとは?? Go 1.16 で導入された機能で、実際のファイルを変更せずに、仮想的に別のファ イルで置き換える仕組み { "Replace": { "/path/to/main.go": "/tmp/mutant_1/main.go"
} } go test -overlay=overlay.json ./... -
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) -
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) -
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 }