品質保証 2026年6月25日

テスト戦略を中小案件の規模で現実的に敷くための考え方と手順

XECIN QAテスト戦略テストピラミッド技術検証

「テストはどこまで書けばいいですか」と若手に聞かれて、即答できなかったことがある。

正直なところ、その案件は予算も期間もカツカツで、「全部テストする」なんて選択肢は最初から無かった。でも「勘で間引く」と品質が読めなくなる。テストは目的ではなく手段なので、手段に予算を食われて目的(納期内に壊れないものを出す)を見失っては本末転倒になる。

この問いに自分なりの答えを出すまでに、一度盛大に失敗している。その話から書く。

最初は「ピラミッドの教科書通り」をやろうとして溶けた

テスト歴がそこそこ長くなると、テストピラミッド(単体が土台、結合が中間、E2Eが頂点)の図は何度も見ている。ある中小案件で、私はこれを真に受けて「単体を分厚く、結合もそこそこ、E2Eも一通り」を全部やろうとした。

結果、テストコードがプロダクトコードより先に膨らみ、仕様が一度変わるたびに大量のテストが赤くなって、その修正だけで半日溶ける状態になった。今思えば、ピラミッドは「比率の目安」であって「全部やれ」という意味ではなかった。中小案件の規模に翻訳しないまま持ち込んだのが間違いだった。

そこから、「どの層に何割の労力をかけるか」を案件規模で決め直すようにした。

テストピラミッドを現場サイズに翻訳する

直近の社内プロダクト(マルチテナントのノート管理アプリ)で実際に敷いた配分がこれ。教科書の比率ではなく、「壊れたときの痛みが大きい順」に労力を寄せている。

何を担保するか労力の目安自動化
API 単体(Jest)入力検証・分岐・例外。ビジネスルールの最小単位約5割必須
API シナリオ(Jest)作成→更新→削除の一連が破綻しないか約2割必須
フロント単体(Vitest)ストア・主要コンポーネントの状態遷移約2割必須
画面 E2E(Playwright・モック型)主要画面の振る舞い。APIはモックで固定約1割必須
実画面の目視確認レイアウト崩れ・体感。chrome-devtools で確認都度手動

カバレッジ目標も「100%」とは置かない。コアロジック(api層)は80%、共通処理(common層)は70%を下限の合意ラインにした。ここは好みが分かれるところですが、中小案件で90%以上を全層に課すと、テストのためのテストが増えて費用対効果が崩れる、というのが現場の実感です。

ポイントは、E2Eを「頂点=少数精鋭」に保つこと。E2Eは書くのも直すのも一番高い。だからAPIをモックで固定した「モック型」にして、画面の振る舞いだけを検証し、実APIとの結合は別枠に切り出した。

テストが1つも無いコードを引き継いだら、まず特性化テスト

中小案件でよくあるのが、「とりあえず動く」状態で引き継いだコードにテストが1行も無いケース。ここでいきなり「正しい仕様」のテストを書こうとすると、そもそも正しい仕様が誰にも分からなくて止まる。

こういうときは、正しさを問わずに「今の挙動」をそのまま固定する特性化テスト(characterization test)から入る。リファクタや改修で挙動が変わったら気づける、という安全網を先に張る考え方です。

// 引き継いだ請求額計算。仕様書は無い。まず「現状の出力」を記録して固定する
import { calcInvoiceTotal } from "../legacy/invoice";

describe("calcInvoiceTotal: 現状の挙動を固定(characterization)", () => {
  // 正しいかどうかは今は問わない。リファクタで変わったら検知することが目的
  it("税込・端数切り捨ての現状出力を記録する", () => {
    expect(calcInvoiceTotal({ unit: 1980, qty: 3, taxRate: 0.1 })).toBe(6534);
    expect(calcInvoiceTotal({ unit: 100, qty: 0, taxRate: 0.1 })).toBe(0);
  });
});

最初に書くのは「一番触る予定の関数」と「壊れたら金額がずれる関数」の2種類だけでいい。全部を特性化しようとすると、また「全部テスト」の罠に戻る。

「重要だが壊れやすい箇所」に寄せる ―― リスクベースで集中する

労力を均等に配ると、たいてい一番こわい所が手薄になる。だから私は「重要度×壊れやすさ」でテストの密度を変える。重要度は「壊れたときの被害」、壊れやすさは「変更頻度と複雑さ」で見る。

このプロダクトで最優先に置いたのは、テナント越境(別テナントのデータが見えてしまう)の防止だった。被害が「情報漏洩」で最大、かつ権限チェックは分岐が多くて壊れやすい。ここだけは網羅的に書いた。

// テナントBのユーザーが、テナントAのノートに一切触れないことを越境テストで担保する
describe("テナント越境: 取得・更新・削除のすべてを拒否する", () => {
  const noteOfTenantA = "note_a_001";

  it.each(["GET", "PUT", "DELETE"])("テナントBは %s で404相当を返す", async (method) => {
    const res = await callApi(method, `/notes/${noteOfTenantA}`, { tenant: "B" });
    // 403で「存在は知られる」より、404で「存在も隠す」を採用
    expect(res.status).toBe(404);
  });
});

逆に、管理画面の表示順の並びみたいな「壊れても被害が小さい所」は、E2Eを1本通すだけにして単体は省いた。テストを「全部に均等」ではなく「こわい所に厚く」配るだけで、同じ工数でも事故の確率がかなり下がる。

手動で残す範囲と、自動化する範囲の線引き

「自動化=善」でもない。自動化は初期コストがかかるので、「何度も回す」「判定が機械的」なものから自動化し、「頻度が低い」「人の目でしか判定できない」ものは手動で残すのが費用対効果として正しい。

種類判定頻度結論
API のバリデーション・分岐機械的(期待値が明確)毎push自動化
画面の振る舞い(押下→遷移)機械的毎push自動化(モック型E2E)
レイアウト崩れ・余白・体感人の目リリース前手動(実画面確認)
文言・トーンの違和感人の判断リリース前手動レビュー

実画面の確認は chrome-devtools で主要ブレークポイントを開いて目視している。ここを無理にスクリーンショット差分の自動化に倒すと、フォントレンダリングの誤差で false positive が頻発して、結局「赤いのを無視する」文化ができてしまう。

# CI(抜粋): 単体→結合→モック型E2E を毎pushで回す
# ※ 改善の余地あり: 現状は実APIとの結合テストをCIに載せておらず、
#   ステージング相当でのスモークは手動で流している。本来はnightlyで
#   実API結合を1日1回回したいが、テストデータの用意が追いついていない。
test:
  steps:
    - run: npm run test:unit       # Jest / Vitest
    - run: npm run test:scenario   # API シナリオ
    - run: npm run test:e2e:mock   # Playwright(page.route でAPIモック)

正直に書くと、この実API結合の自動化が積み残しになっているのは、中小案件あるあるだと思う。テストデータの整備という地味な投資が後回しになりがちなんですよね。

振り返って

テスト戦略は「全部やる/勘で間引く」の二択じゃない。ピラミッドを案件規模に翻訳し、特性化テストで足場を作り、こわい所に密度を寄せ、手動と自動を費用対効果で線引きする ―― この4つを順番に決めるだけで、限られた工数でも「どこを守れているか」が言語化できる。

受け入れ基準の形にしておくと、引き継ぎや見積もりでもブレない。たとえば今は「api層カバレッジ80%以上」「テナント越境テストが全エンドポイントで緑」「主要画面のモック型E2Eが緑」を、この種のプロダクトの最低ラインとして握るようにしている。動いた=品質OK ではない、を測れる形にしておきたい。

次にやりたいのは、積み残している実API結合のnightly自動化と、リスク評価(重要度×壊れやすさ)を毎スプリントで見直す運用の定着。テスト戦略も一度敷いて終わりではなく、案件が育つにつれて配分を組み替えていきたい。