Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Who tests the Tests ?
Search
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
sivchari
February 26, 2026
0
46
Who tests the Tests ?
sivchari
February 26, 2026
Tweet
Share
More Decks by sivchari
See All by sivchari
govalid ~ Type-safe validation tool ~
sivchari
0
46
Go1.25 リリースパーティ ~ nil pointer bug ~
sivchari
0
46
Google Developer Group - DevFest Tokyo 2025
sivchari
0
46
Go 1.26 リリースパーティ
sivchari
0
47
静的解析 x Kubernetes API Conventions = Kube API Linter ~ ベストプラクティスに準拠したカスタムリソースの作り方と運用 ~
sivchari
0
110
What's GOCACHEPROG ?
sivchari
1
470
gh_extensionsによる快適なOSS生活.pdf
sivchari
0
79
Visualization Go scheduler by gosched-simulator
sivchari
1
540
protoc pluginのはじめかた
sivchari
0
82
Featured
See All Featured
Dominate Local Search Results - an insider guide to GBP, reviews, and Local SEO
greggifford
PRO
0
92
Docker and Python
trallard
47
3.7k
SEO Brein meetup: CTRL+C is not how to scale international SEO
lindahogenes
0
2.4k
Navigating the moral maze — ethical principles for Al-driven product design
skipperchong
2
270
Future Trends and Review - Lecture 12 - Web Technologies (1019888BNR)
signer
PRO
0
3.2k
YesSQL, Process and Tooling at Scale
rocio
174
15k
The State of eCommerce SEO: How to Win in Today's Products SERPs - #SEOweek
aleyda
2
9.7k
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
162
16k
The untapped power of vector embeddings
frankvandijk
2
1.6k
For a Future-Friendly Web
brad_frost
183
10k
A brief & incomplete history of UX Design for the World Wide Web: 1989–2019
jct
1
310
Java REST API Framework Comparison - PWX 2021
mraible
34
9.2k
Transcript
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通ったのでレビューお願いします」 ◦ 「CI失敗しているので直してください」 CIはチーム開発をする上での最低限の約束事として動いている ◦
「新規開発をするから GitHub Actionsから整えるぞ!」
CIは開発サイクルの最低限の約束
CIは開発サイクルの必須の約束 AI Agentが自己回帰する際のチェックポイントになる ◦ 「CIが失敗したので調査をして継続します」 ◦ 「CIが全てPASSしたので終了します」 出力するコードが指数関数的に増える中で自動で品質を担保することはより必 要な世界線で開発をしている
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
「ある指標が目標になると、その時点でその指標は “良い指標”ではなくなる」
数値だけを追い求めるケース タイピング速度が速くなると開発生産性が上がる → タイピング速度に固執する PRレビューの粒度は小さいとレビューしやすくていい → PRの数を貢献した指標としてしまうと分割することが目標となる テストのカバレッジを常に 80%維持する →
本質的に必要のないテストケースの増加へつながる
数値だけを追い求めるケース タイピング速度が速くなると開発生産性が上がる → タイピング速度に固執する PRレビューの粒度は小さいとレビューしやすくていい → PRの数を貢献した指標としてしまうと分割することが目標となる テストのカバレッジを常に 80%維持する →
本質的に必要のないテストケースの増加へつながる
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) }
AI時代だと類似した出来事が起こりうることがある Xのテスト書いて わかりました t.Skipしてる!? ごめんなさい
Mutation Testing
Mutation Testingとは プログラムの一部を 意図的に書き換え 、生成したミュータントに対してテスト を実行し、失敗するかどうかを検証する • 演算子の反転 : *
→ / , - → + , == → != , || → && • 条件の変換 : elseの削除, caseの削除 • ループの変換 : continue → break
Mutation Testingの評価方法 ミューテーションテストの実行後 • テストが失敗 → ミュータントが KILL(成功) • テストが成功
→ ミュータントが SURVIVED(失敗) テストの指標は以下に多くのミュータントを KILLできたかで考える
Mutation Testingの注意事項 実行時間とのトレードオフ • Mutation Testingではソースコードの修正が必須 • 規模が大きいと実行時間も伸びるためどこに対して行うかを考える 100% KILLEDは目指さない
• 1 / 1 → 1 * 1 のように変換しても意味がない場合もある • そのケースが必要かどうかを検討しながら時には Ignoreする 「ある指標が目標になると、その時点でその指標は “良い指標”ではなくなる」
どのように Mutation Testingを実装するのか
Mutation Testingの実装方法 sivchari/gomuの実装方法をベースとします GoのMutation Testingライブラリ overlayを活用しミューテーションテストを並列に実行する インクリメンタル解析を行い差分のみの Mutation Testingを実行可能 PRへのレポート生成と閾値以下のスコアで
FailさせるCIファースト思想
Mutation Testingの挙動 パースして AST生成 ↓ Mutation生成 ↓ Mutationを適用して一時ファイルと overlay.jsonを生成 ↓
go test -overlay=overlay.json ./... を行いKILLED/SURVIVED判定
パースして AST生成 main.go → parser.ParseFile → AST 1 + 1
→
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 })
Mutate switch n := node.(type) { case *ast.BinaryExpr: return m.mutateBinaryExpr(n,
pos) }
Mutate switch n := node.(type) { case *ast.BinaryExpr: return m.mutateBinaryExpr(n,
pos) }
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 listをgoroutineで実行する
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 ./...
なぜgomuでoverlay? 直接ファイルを修正すると途中で失敗すると戻せなくなる 失敗した場合に mutatedした後のソースファイルをデバッグとして残したい 実行時に置き換えるので goroutineで並列or並行に実行しても競合しない
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 }
まとめ
まとめ • AI時代、加速する環境の中、品質も加速して変わり続ける • テストカバレッジは重要、同様に意味のあるテストかどうかも重要 • Mutation Testingを活用してテストをテストしよう
ありがとうございました!