Next.jsの本番ビルド(npm run build + npm run start)でE2Eテストの認証モックを動作させる方法を解説します。

外部認証パッケージのセキュリティ制限をバイパスせずに、アプリ側で認証スキップを実装するパターンです。

## こんな人におすすめ

- Next.jsの本番ビルドでE2Eテストを実行したい方
- 認証モックがCI環境で動作しない問題に困っている方
- 外部認証パッケージのセキュリティ設計を尊重したい方
- React Hooksのルールを守りながら環境変数を活用したい方

## 目次

- [問題](#問題)
- [解決策](#解決策)
- [ポイント](#ポイント)
- [CI/CD設定(GitHub Actions)](#cicd設定github-actions)
- [よくある間違いと解決方法](#よくある間違いと解決方法)
- [実装手順ステップバイステップ](#実装手順ステップバイステップ)
- [トラブルシューティング](#トラブルシューティング)
- [まとめ](#まとめ)

## 詳細

### 問題

CI環境でE2Eテストを本番ビルドで実行した際に、私自身がこの問題に遭遇しました。

外部SSO認証パッケージは、本番環境(NODE_ENV=production)ではskipAuth設定を**意図的に無視**します。

これはセキュリティ対策として正しい設計です。

しかし、CI環境でE2Eテストを本番ビルドで実行すると、認証モックが効きません。

結果としてcurrentUsernullになり、認証テストが失敗します。

### 解決策

アプリケーションのuseAuthフックで、ビルド時に埋め込まれる環境変数NEXT_PUBLIC_SKIP_AUTHをチェックし、モックユーザーを直接返します。

``typescript
// hooks/useAuth.ts
const SKIP_AUTH = process.env.NEXT_PUBLIC_SKIP_AUTH === 'true';

export function useAuth() {
// Hooksは条件分岐の前に全て呼び出す(React Hooksのルール)
const [session, setSession] = useState<Session | null>(null);
const { isLoading: authProviderLoading } = useSession();

const currentUser = useMemo<User | null>(() => {
if (SKIP_AUTH) {
return MOCK_USER; // モックユーザーを直接返す
}
return session ? mapSessionToUser(session) : null;
}, [session]);

useEffect(() => {
if (SKIP_AUTH) return; // スキップ時は認証チェックしない
// ... 通常の認証フロー
}, [authProviderLoading]);

// 認証スキップモード時は早期リターン
if (SKIP_AUTH) {
return {
currentUser: MOCK_USER,
isLoading: false,
// ...
};
}

return { currentUser, isLoading, /* ... */ };
}
`

### ポイント

重要なポイントは以下の3つです。

1. **React Hooksのルールを守ります**:
if (SKIP_AUTH) return ... は全てのHooksを呼び出した**後**に配置します
2. **ビルド時に値が決定します**:
NEXT_PUBLIC_*環境変数はビルド時に埋め込まれるため、本番ビルドでも正しく動作します
3. **外部パッケージを変更しません**: セキュリティ設計を尊重し、アプリ側で制御します

### CI/CD設定(GitHub Actions)

`yaml
jobs:
e2e-auth:
env:
NEXT_PUBLIC_SKIP_AUTH: 'true' # ビルド時に埋め込まれる
steps:
- run: npm run build # この時点で環境変数が埋め込まれる
- run: npm run test:e2e:auth
`

### よくある間違いと解決方法

実装時につまずきやすいポイントを3つ紹介します。

**❌ 間違い1: React Hooksの呼び出し順序を守らない**

`typescript
// ❌ NG: 条件分岐の前にreturnしている
export function useAuth() {
if (SKIP_AUTH) {
return { currentUser: MOCK_USER, isLoading: false };
}

const [session, setSession] = useState<Session | null>(null);
// ...エラー: Hookが無条件に呼ばれていない
}

// ✅ OK: 全てのHookを呼び出した後に条件分岐
export function useAuth() {
const [session, setSession] = useState<Session | null>(null);
const { isLoading: authProviderLoading } = useSession();

if (SKIP_AUTH) {
return { currentUser: MOCK_USER, isLoading: false };
}
// ...以下通常処理
}
`

**❌ 間違い2: 環境変数がビルド時に埋め込まれていない**

`bash
# ❌ NG: 環境変数を設定せずにビルド
npm run build
npm run test:e2e:auth
# SKIP_AUTH が false になる

# ✅ OK: ビルド前に環境変数を設定
NEXT_PUBLIC_SKIP_AUTH=true npm run build
npm run test:e2e:auth
# SKIP_AUTH が true に埋め込まれる
`

**❌ 間違い3: 外部認証パッケージの処理をスキップしていない**

`typescript
// ❌ NG: パッケージの処理を呼び出してしまう
export function useAuth() {
const [session, setSession] = useState<Session | null>(null);
const { isLoading, user } = useSession(); // ← スキップ時も実行

if (SKIP_AUTH) {
return { currentUser: MOCK_USER, isLoading: false };
}
}

// ✅ OK: useEffect内でスキップ判定
export function useAuth() {
const [session, setSession] = useState<Session | null>(null);
const { isLoading } = useSession();

useEffect(() => {
if (SKIP_AUTH) return; // スキップ時は処理しない
// ...通常の認証チェック
}, []);
}
`

### 実装手順ステップバイステップ

段階的に実装するための手順を解説します。

**Step 1: 環境変数の設定(2分)**

GitHub Actionsの設定ファイル(
.github/workflows/e2e-test.yml)に環境変数を追加します。

`yaml
name: E2E Tests

on: [push, pull_request]
jobs:
e2e:
runs-on: ubuntu-latest
env:
NEXT_PUBLIC_SKIP_AUTH: 'true' # ← ここに追加
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npm run build # 環境変数がビルド時に埋め込まれる
- run: npm run test:e2e
`

**Step 2: useAuthフックの修正(5分)**

hooks/useAuth.tsを修正して、モック認証をサポートします。

`typescript
// hooks/useAuth.ts
const SKIP_AUTH = process.env.NEXT_PUBLIC_SKIP_AUTH === 'true';

const MOCK_USER: User = {
id: 'mock-user-123',
email: 'test@example.com',
name: 'Test User',
role: 'admin',
};

export function useAuth() {
const [session, setSession] = useState<Session | null>(null);
const { isLoading: authProviderLoading } = useSession();

const currentUser = useMemo<User | null>(() => {
if (SKIP_AUTH) {
return MOCK_USER;
}
return session ? mapSessionToUser(session) : null;
}, [session]);

useEffect(() => {
if (SKIP_AUTH) return;
// 通常の認証ロジック
checkSession().then(setSession);
}, [authProviderLoading]);

if (SKIP_AUTH) {
return {
currentUser: MOCK_USER,
isLoading: false,
error: null,
logout: async () => {},
};
}

return {
currentUser,
isLoading: authProviderLoading,
error: null,
logout: async () => await signOut(),
};
}
`

**Step 3: テスト設定の確認(3分)**

Playwrightのテスト設定が本番ビルドをサポートしていることを確認します。

`typescript
// playwright.config.ts
export default defineConfig({
webServer: {
command: 'npm run start', // 本番ビルドをサーブ
port: 3000,
timeout: 120_000,
},
testDir: './e2e',
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
});
`

**Step 4: 検証テストの実行(5分)**

`bash
# ローカルで検証
NEXT_PUBLIC_SKIP_AUTH=true npm run build
npm run start &
npm run test:e2e

# CI環境での検証(GitHub Actions)
git push origin feature/auth-mock
# GitHub Actionsが自動実行
`

### トラブルシューティング

実装後に発生しやすい問題と解決方法を紹介します。

**問題1: SKIP_AUTHが反映されない場合**

現象: 環境変数を設定してもSKIP_AUTHがfalseのままの場合があります。

原因: キャッシュされたビルド結果を使用している、または環境変数の設定タイミングが間違っている。

解決方法:

`bash
# キャッシュをクリアして再ビルド
rm -rf .next
NEXT_PUBLIC_SKIP_AUTH=true npm run build

# または、環境変数の設定を確認
echo $NEXT_PUBLIC_SKIP_AUTH # true が出力されることを確認
`

**問題2: モックユーザーが返されない場合**

現象: useAuthがモックユーザーではなくnullを返す。

原因: React Hooksのルールに違反している、またはSKIP_AUTH定数の判定が正しくない。

解決方法:

`typescript
// useAuth内でDOMで確認
useEffect(() => {
console.log('SKIP_AUTH:', SKIP_AUTH);
console.log('currentUser:', currentUser);
}, []);

// useAuth呼び出しコンポーネント
const { currentUser } = useAuth();
console.log('Final currentUser:', currentUser);
`

**問題3: ローカルでは動作するが、CI/CDで失敗する場合**

現象: ローカルでは正常に動作しているのにCI環境で失敗。

原因: 環境変数がCI環境に正しく設定されていない。

解決方法:

`yaml
# .github/workflows/e2e-test.yml
env:
NEXT_PUBLIC_SKIP_AUTH: 'true'

# または、.env.test ファイルを使用
steps:
- run: echo "NEXT_PUBLIC_SKIP_AUTH=true" > .env.local
- run: npm run build
`

## まとめ

Next.jsの本番ビルドで認証モックを動作させるには、外部認証パッケージのセキュリティ設計を尊重しながら、アプリ側で対応するのがベストプラクティスです。

**重要なポイント:**
- 外部認証パッケージが本番環境でskipAuthを無視するのはセキュリティ上正しい設計です
- アプリ側の
useAuthフックでNEXT_PUBLIC_SKIP_AUTH`をチェックすることで対応できます
- ビルド時に環境変数が埋め込まれるため、CI/CDで安定したE2Eテストが実現できます
- React Hooksのルールを守ることが実装のカギになります

**実装ステップ:**
1. GitHub ActionsでNEXT_PUBLIC_SKIP_AUTH環境変数を設定
2. useAuthフックを修正してSKIP_AUTH定数をチェック
3. モックユーザーを定義して返す
4. CI環境で検証テストを実行

この手法を採用することで、セキュリティと開発効率のバランスが取れた堅牢なテスト環境が実現できます。

## 参考リンク

**公式ドキュメント:**
- [Next.js Environment Variables](https://nextjs.org/docs/app/building-your-application/configuring/environment-variables)
- [React Hooks Rules](https://react.dev/reference/rules/rules-of-hooks)
- [GitHub Actions: Environment Variables](https://docs.github.com/en/actions/learn-github-actions/environment-variables)
- [Playwright Configuration](https://playwright.dev/docs/test-configuration)

---