AI時代の品質戦略として、Integration Test を主戦場にした話
SaaS開発を続けていると、
ある時から徐々に「E2Eテストの限界」が見えてくる。
最初は便利だ。
- ユーザー操作に近い
- 実際の挙動を確認できる
- 安心感がある
だからE2Eは増えやすい。
しかし、プロダクトが成長すると、
徐々に別の問題が発生する。
- flaky test
- 長い実行時間
- 並列競合
- データ依存
- UI変更による大量破壊
- 原因調査コスト増加
特に辛いのが、
「なぜ失敗したのか分からない」
状態だ。
AI時代、この問題はさらに悪化する
AI Coding によって、
コード変更量そのものが増え始めている。
つまり:
- PR数
- 修正頻度
- 実験速度
- 機能追加速度
がさらに増加する。
すると、
「最後に人が頑張って確認する」
モデルは徐々に破綻する。
特に:
- Redis Pub/Sub
- Queue
- Worker
- 非同期処理
- 外部API
- Push通知
のようなシステムでは、
UIだけでは品質保証できない。
実際に壊れるのは“境界”
現場で本当に壊れるのは、
大抵「システム間の境界」だった。
例えば:
- Redis publish 後に worker が処理しない
- retryで重複送信
- DB更新漏れ
- timeout時の不整合
- 外部API失敗時の中途半端状態
- idempotency崩壊
つまり問題は:
「画面」
ではなく、
「システム境界」
に集中していた。
そこで、Integration Test を主戦場に変えた
E2Eを増やすのではなく、
「壊れやすい境界を固定する」
方向へ変えた。
重点的に守ったのは:
- Redis
- DB
- Worker
- Event
- Queue
- 外部API
だった。
実際の Integration Test 例
例えば Push通知基盤では、
こんな流れを固定した。
Redis Publish
↓
Worker 起動
↓
Message Build
↓
LINE API 呼び出し
↓
DB更新
↓
History更新
重要なのは:
- UIを通さない
- 実処理は通す
- 外部APIだけMockする
こと。
実際のテストコード例
例えば Jest + Redis + nock を使うと、
かなり実運用に近いテストが書ける。
Redis Event Publish
import Redis from 'ioredis'
const redis = new Redis(process.env.REDIS_URL)
await redis.publish(
'notification',
JSON.stringify({
event_name: 'push-notification',
notification_id: 1001,
})
)
これは実際に worker が購読している channel に流す。
重要なのは:
「controller を直接叩かない」
こと。
実際の event flow を通す。
LINE API Mock
外部APIは nock で固定する。
import nock from 'nock'
nock('https://api.line.me')
.post('/v2/bot/message/multicast')
.reply(200, {
success: true,
})
これで:
- API成功
- timeout
- 500 error
- retry
なども再現できる。
DB副作用を確認する
実際に重要なのは、
レスポンスではなく副作用。
例えば:
const history = await NotificationHistory.findOne({
notification_id: 1001,
})
expect(history.push_flg).toBe(1)
これで:
- 正常送信されたか
- DB更新されたか
を確認する。
重複送信を防ぐテスト
非同期処理で怖いのが、
idempotency崩壊。
例えば:
const histories = await NotificationHistory.find({
notification_id: 1001,
push_flg: 1,
})
expect(histories.length).toBe(1)
これで:
「同じ通知が2回送られていない」
ことを保証する。
実運用ではかなり重要。
timeout と retry も確認する
例えば外部API timeout。
nock('https://api.line.me')
.post('/v2/bot/message/multicast')
.delay(5000)
.reply(504)
その後:
expect(history.push_flg).toBe(0)
expect(history.pending_flg).toBe(1)
を確認する。
つまり:
- 中途半端成功になっていない
- retry可能状態か
を保証する。
Docker Compose で CI に組み込む
CIでは docker-compose を使った。
version: '3'
services:
redis:
image: redis:7
worker:
build: .
mongo:
image: mongo:6
GitHub Actions 側は:
- name: Run Integration Test
run: |
docker compose up \
--abort-on-container-exit \
--exit-code-from worker
これで:
- Redis
- Worker
- DB
込みで実行できる。
E2Eをゼロにするわけではない
ここまで読むと、
「じゃあE2E不要なのでは?」
と思われるかもしれない。
しかし、それは違う。
E2Eには、
Integration Testでは守り切れない領域が存在する。
なぜE2Eが必要なのか
Integration Test は強力だ。
特に:
- Redis
- DB
- Queue
- Worker
- 外部API
- 非同期処理
などの境界品質を守るには非常に向いている。
しかし一方で、
「実際のユーザー操作として成立しているか」
は別問題になる。
システムは正しくても、ユーザー導線は壊れる
例えば:
- ボタンが押せない
- Router設定ミス
- Form submitされない
- CSP設定でscriptが落ちる
- Cookie設定不備
- 認証導線崩壊
- ブラウザ差異
- Frontend build設定ミス
これらは、
Integration Test だけでは検知しづらい。
つまり:
「内部ロジックは正常」
でも、
「ユーザーは使えない」
状態は普通に起こる。
特に“接続部分”はE2Eが強い
例えばログイン。
ログインは:
- Frontend
- Cookie
- Session
- CSRF
- Backend
- Reverse Proxy
- Browser
- Redirect
など、
複数層を跨ぐ。
これはE2Eがかなり強い。
例えば:
Feature('login')
Scenario('user can login', async ({ I }) => {
I.amOnPage('/login')
I.fillField('email', 'test@example.com')
I.fillField('password', 'password')
I.click('Login')
I.see('Dashboard')
})
このテスト自体は単純だが、
- session
- cookie
- redirect
- browser state
全部をまとめて確認できる。
これはIntegration Test単体では難しい。
“主要導線の死活監視”として重要
E2Eの価値は、
「細かい仕様確認」
より、
「主要導線が死んでいないか」
にある。
例えば:
- 新規登録
- ログイン
- 決済
- 購入
- 投稿
- Push送信
- 管理画面アクセス
など。
つまり:
「ビジネス的に止まると困る導線」
を最終確認する役割。
E2Eを増やしすぎると逆に壊れる
問題は、
ここを全部E2Eでやろうとすること。
例えば:
- バリデーション全パターン
- 全権限
- 全UI状態
- 全異常系
- 全ブラウザ
- 全分岐
をE2E化すると:
- flaky増加
- CI遅延
- 保守不能
- 調査困難
になる。
つまり:
「品質を守るためのテストが、開発速度を壊す」
状態になる。
これは本末転倒。
E2Eは“最小限の高価値導線”へ絞る
だから現在は:
| テスト種類 | 主戦場 |
|---|---|
| Static Check | 文法・型 |
| Unit Test | 純粋ロジック |
| Integration Test | システム境界 |
| E2E | 最重要導線のみ |
という形へ寄せている。
E2Eは:
「本当に壊れると困る導線」
だけを守る。
E2Eは“安心感”を提供する
もう一つ重要なのは、
E2Eには心理的価値があること。
例えば:
- 実際に画面が動く
- ログインできる
- ボタン押せる
- 決済できる
これは、
エンジニア・PM・QA・CS含めて安心感がある。
つまりE2Eは:
“技術確認”だけでなく、“運用安心感”も担っている。
AI時代ほど、“構造”が重要になる
AIによって、
コード生成速度はさらに上がる。
だが:
- 非同期
- consistency
- retry
- idempotency
- 副作用
- event ordering
この辺は、
今後も人間が設計する必要がある。
だから重要なのは:
「人間が頑張る」
ではなく、
「壊れやすい場所を構造で固定する」
ことだと思っている。
最後に
品質保証は、
「最後に頑張る人」を増やすことではない。
むしろ逆で、
“頑張らなくても壊れにくい構造”
を作ることだと思う。
そして自分にとって Integration Test は、
単なるテスト手法ではなく、
「壊れやすい境界を、継続的に固定し続けるための仕組み」
だった。
AI時代によって、
コード量も変更速度もさらに増えていく。
だからこそ今後は、
「人が気合で確認する品質保証」ではなく、
“構造によって再現可能に守られる品質”
が、
より重要になっていくと思っている。