1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

E2Eを増やし続けると、品質保証は壊れていく

1
Posted at

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時代によって、
コード量も変更速度もさらに増えていく。

だからこそ今後は、

「人が気合で確認する品質保証」ではなく、

“構造によって再現可能に守られる品質”

が、
より重要になっていくと思っている。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?