検収が終わるまで品質が分からない——という状況、受託案件をやっている方なら一度は経験があると思います。
私も長い間それが「普通」だと思っていました。コードが書き終わったらテスターに渡して、バグが出たら直す。その繰り返し。でも、ある案件で手戻りが連続して限界を感じたのが、シフトレフトQAを本格的に整備するきっかけでした。
「動いた=品質OK」が通用しなくなった案件
きっかけは2年ほど前に担当したWebアプリのリニューアル案件です。
7人チームで3か月の開発期間。スプリントごとに機能を積み上げていく想定でしたが、最終的に検収フェーズで30件超の指摘が来ました。その中には「機能が仕様書と違う」というものが10件以上含まれていて、スプリントレビューで通ったはずの機能が何件も差し戻しになりました。
開発者は「動いてますよ」と言う。確かに動いている。でも、クライアントの期待値とズレている。
動いた=品質OKではない、というのはその時に骨身に染みた感覚です。根本原因は開発者の技術力ではなく、「何をもって完了とするか」の基準が曖昧なまま進んでいたことでした。
コストを考えると、早く見つけるしか合理的な選択肢がない
「シフトレフト」を一言で言うなら、バグを見つけるタイミングをできるだけ早くする、これだけです。
なぜ早い方がいいのか。コストが全然違うからです。
| 検出タイミング | 修正コストの目安(相対値) |
|---|---|
| 要件定義・設計段階 | 1 |
| 実装中(コードレビュー) | 5〜10 |
| テスト工程 | 20〜50 |
| 検収・本番後 | 100〜1000 |
数値はよく引用されるものですが、実感としても「検収後に発見した仕様バグを直す工数」は設計段階で防いだ場合の数十倍かかります。直すだけなら短時間でも、「変更 → 影響調査 → 再テスト → 再検収」という連鎖が重い。
ここを変えないと、頑張っても頑張ってもリリース直前に詰まるという構造が変わらない。
まず受け入れ基準を「先に書く」ことにした
最初の一手として取り組んだのが、受け入れ基準(Acceptance Criteria)をチケット作成時に必ず書くルールです。
それ以前のチームでは受け入れ基準はほぼ「動作確認OK」の一行でした。改善後のチケットフォーマットはこんな形です:
## タスク
ユーザーがパスワードをリセットできるフォームを実装する
## 受け入れ基準
- [ ] 存在しないメールアドレスを入力した場合、エラーメッセージが表示される
(「入力されたメールアドレスは登録されていません」)
- [ ] 有効なメールアドレス入力後、リセットメールが60秒以内に届く
- [ ] リセットリンクの有効期限は24時間。期限切れは専用エラーページへ遷移する
- [ ] 連続5回失敗でアカウントが30分ロックされ、管理者にメール通知が届く
- [ ] iPhone SE / Pixel 6 のChromeで入力フォームが崩れない
受け入れ基準を書くのは開発者ではなく、担当PM(または私)が仕様書から起こす形にしました。実装者が書くと、どうしても「実装できる範囲」に引っ張られてしまう傾向があるためです。
最初のアプローチが甘かった部分を正直に言うと、「各開発者がチケット着手時に書く」ルールにしていたんですが、忙しいスプリント中盤になると受け入れ基準の品質がバラバラになっていきました。「担当PMが先行して書く」に変えてから安定しました。
想定外の副産物:「書けない」が品質バグを事前検知していた
受け入れ基準の先付けを始めて最初の2スプリントで、予期しないことが起きました。
受け入れ基準が書けないチケットが出てきた。
「ユーザーが商品をお気に入り登録できる」というチケットに対して、受け入れ基準を書こうとしたPMが「このお気に入り、ログアウト後も保持されるんだっけ?」と詰まったんです。仕様書を見直したら、そこが決まっていなかった。
受け入れ基準が書けなかった理由は「仕様が未確定だったから」でした。
もし受け入れ基準を後回しにしていたら、この曖昧さは実装が終わるまで表面化しなかったはずです。最初の2スプリントだけで4件のこうしたケースが見つかり、どれも実装前にクライアントへ確認して解決できました。この2スプリントの検収指摘件数は前回比で約40%減でした。
CI と静的解析をどの工程に置いたか
受け入れ基準の先付けと並行して整備したのが、コードレビューと静的解析のタイミングです。
以前は「コードが書き終わったらPRを出す → レビュー → テスト → 検収」という直列の流れでした。これを、PRを出した瞬間に型チェックと静的解析が走るように変えました。
# .github/workflows/ci.yml(改善後の抜粋)
# ※ 改善の余地あり: biomeのlintルールはプロジェクト固有の禁止パターン
# (any型の使用禁止・console.log禁止など)をまだ追加していない。
# デフォルトルールのみで回しているため、追加設定で検出精度を上げる余地がある。
on:
pull_request:
branches: [main, develop]
jobs:
quality-gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- name: 型チェック
run: npx tsc --noEmit
- name: 静的解析 (biome)
run: npx biome check --error-on-warnings src/
- name: ユニットテスト
run: npm run test:unit -- --coverage
- name: カバレッジ下限チェック
run: npx istanbul check-coverage --statements 60
「PRを出す」という行動が自動的に品質ゲートを通過する設計にしておくと、「型エラーのまま出すのは恥ずかしい」という雰囲気が自然に醸成されていきます。押し付けではなく構造で誘導する、というのが狙いでした。
コードレビューのチェックリストも合わせて整備しました:
## レビューチェックリスト(PR提出前の自己確認)
- [ ] 受け入れ基準の全項目を満たすことを確認したか
- [ ] エラーケース(空入力・不正値・ネットワーク断)を考慮したか
- [ ] 変更が既存機能に影響する場合、その範囲を記載したか
- [ ] ユニットテストが追加または更新されているか
これ、形式的になるだけでは?と最初は思っていたんですが、「受け入れ基準の全項目を確認したか」のチェックが意外と効きました。実装者が自分でACを読み直すきっかけになって、「あ、この条件見落としてた」という発見が何件かありました。
手戻り率を数字で追いかけた
「改善している気がする」を「改善している」に変えるために、チームで2つの指標を定義しました。
| 指標 | 定義 | 計測タイミング |
|---|---|---|
| 手戻り率 | 検収指摘件数 ÷ 全チケット数(スプリント単位) | スプリント毎 |
| 検収時指摘件数 | 検収フェーズで出た指摘の絶対数 | リリース毎 |
最初の計測結果が出た時、「手戻り率12%」という数値が出ました。社内で「これは高い?低い?」という話になって、それまで誰もこの指標を持っていなかったことに気づきました。測れないものは改善できない、という言葉の重みを感じた瞬間でした。
3か月後には手戻り率が5%を下回り、検収指摘件数も20件超から8件に減っていました。絶対値より「下がり続けているトレンド」を見えるようにしたことで、「やっていることが効いている」という実感をチームで共有できたのが大きかったと思います。
振り返って:定着したことと、まだ難しいこと
3か月試してみて、定着したものとそうでないものが分かれました。
定着したこと
受け入れ基準の先付けは「これなしでチケットを着手するのが怖い」とメンバーが言うくらい浸透しました。CIでの型チェックと静的解析は設定してしまえばゼロコストで回り続けます。手戻り率の可視化は毎スプリントのレトロスペクティブで見ることが習慣になりました。
まだ難しいこと
ユニットテストのカバレッジ管理は、60%という下限は引けたものの、「意味のあるテストを書くかどうか」はレビュー依存になっている部分があります。受け入れ基準の「粒度」も、担当によって観点の抜け漏れが出ることがあります。
テストは目的ではなく手段なので、「カバレッジが上がった」ことより「検収指摘が減った」ことに目を向けるようにしています。「品質は工程で作り込む」というのはスローガンではなく、受け入れ基準を先に書く・CIを通らないとPRを出せない・数字で追う、という具体的な動作の集合です。まだ改善の余地はありますが、今のところこれが私たちの答えです。