この記事はシリーズ「動いているリポジトリを読む — Next.js 16 で学ぶクリーンアーキテクチャと TDD」の第 2 回(全 19 回)です。
実際に動いている公開リポジトリ kai-kou/gem-hunter(MIT)を1本まるごと読み解く連載です。架空のサンプルではなく実ファイルを引用し、掲載しているコマンド結果はすべて実際に動かして採取しています。
全体を通しで読みたい方へ: 同じ内容を 1 冊にまとめた Zenn Book を無料で公開しています。
シリーズ全体の目次(クリックで開く)
- 第 1 回 動いているリポジトリを読むという学び方(公開予定)
- 第 2 回 clone からテストが緑になるまで(この記事)
- 第 3 回 要件IDとADR — 何を解くアプリなのかを地図にする(公開予定)
- 第 4 回 App Router の地図 — フォルダがそのまま URL になる(公開予定)
- 第 5 回 Server Component と Client Component の境界は6ファイルしかなかった(公開予定)
- 第 6 回 なぜ層を分けるのか、分けない選択肢との比較で(公開予定)
- 第 7 回 domain層 — ブランド型で「検証済みの値」を型にする(公開予定)
- 第 8 回 usecases層 — DIコンテナを使わない依存性逆転(公開予定)
- 第 9 回 infrastructure層 — 外部APIの語彙を持ち込ませない翻訳層(公開予定)
- 第 10 回 依存規則を440行のPythonで機械検査する(わざと壊してみる)(公開予定)
- 第 11 回 URLのクエリが画面に出るまでを1本追跡する(公開予定)
- 第 12 回 ライブラリなしの i18n と、変化を目で見ていない人へ伝える実装(公開予定)
- 第 13 回 テスト戦略とTDD — どの層を何でテストするか(公開予定)
- 第 14 回 失敗するテストを先に書く — Red→Green を1周する(公開予定)
- 第 15 回 MSWとTesting Library — 「実ネットワークに出ない」を設定1行で保証する(公開予定)
- 第 16 回 Playwright E2E — 外部APIに依存しないブラウザテストの構成(公開予定)
- 第 17 回 赤くなったテストをどう判定するか — axeの限界とflakyの正体(公開予定)
- 第 18 回 生IPを残さないレート制限・Cookie暗号化・第三者HTMLの表示(公開予定)
- 第 19 回 CIは道具ではなくゲートの集合 — そして自分のプロジェクトへの持ち帰り方(公開予定)
TL;DR
-
git clone→npm ci→npm testで、環境変数ゼロのまま 81 ファイル・954 件のユニット/結合テストが緑 になります(E2E はこの回ではe2e/sp-1.spec.tsの 1 本だけ) - 外部に出ないのは偶然ではなく、2 つの仕組みで構造的に塞いである からです
- ただし
npm run devは 執筆時点で 500 を返します(Turbopack の CSS パースエラー)。回避策つきで説明します
対象読者は、手元で実際にリポジトリを動かしながら読み進めたいエンジニアです。Node.js 22 と git があれば、外部サービスの登録も API キーも要りません。
必要なもの
2 つだけです。
- Node.js 22 以上(執筆環境は v22.22.2)
- git
データベースも Docker も要りません。GitHub のアカウントもこの回では使いません。
動かす
git clone https://github.com/kai-kou/gem-hunter
cd gem-hunter
npm ci
npm ci は package-lock.json に固定されたバージョンをそのまま入れます。Next.js + React + Vitest + Playwright + Tailwind が一式入るので、回線とマシン次第で数十秒はかかります。
続けてテストを走らせます。
npm test
Test Files 81 passed (81)
Tests 954 passed (954)
81 ファイル・954 ケースが全部通ります。 ここまでで、環境変数は 1 つも設定していません。
この件数は基準コミット 39ba4a1(第 1 回で固定した版)のものです。最新の main を clone した場合は増えているはずなので、数が違っても壊れてはいません。
なぜ環境変数なしで通るのか
普通、GitHub API を叩くアプリのテストを何も設定せずに走らせたら、認証エラーかレート制限で落ちます。落ちないのは、テストが外部ネットワークに出ないよう構造的に塞いである からです。
塞ぎ方は 2 つあります。
- ユニットテストは、そもそも外部と話す部品を渡さない。外部と話す処理は「ポート」という約束(インターフェース)越しに呼ばれていて、テストではその場で書いた偽物を渡します(第 8 回・第 14 回)
- 外部と話す部品自体のテストは、HTTP のレイヤーで止める。MSW というライブラリでリクエストを横取りし、定義していないリクエストが飛んだらテストを失敗させる 設定になっています(第 15 回)
2 番目が効いています。「たまたま今日はネットワークが繋がったからテストが通った」という状態を、設定で禁止しているわけです。
開発サーバーでつまずく(執筆時点)
npm run dev
Next.js 16 の next dev は Turbopack で動きます。起動は速く、0.4 秒で待ち受けに入りました。
▲ Next.js 16.3.1 (Turbopack)
- Local: http://localhost:3000
✓ Ready in 383ms
ところが http://localhost:3000/ja を開くと、執筆時点では 500 が返りました。
GET / -> 307 (redirect to: http://localhost:3000/ja)
GET /ja -> 500
サーバー側のログはこうです。
⨯ ./app/globals.css:2826:47
Error: Parsing CSS source code failed
> 2826 | .\[\&_svg\:not\(\.\.\.\)\]\:size-3 svg:not(...) {
| ^
Expected identifier in class selector, got Delim('.')
何が起きているのか
app/globals.css は 230 行しかありません。エラーが指す 2826 行目は、Tailwind CSS v4 がビルド時に生成した CSS の中の位置です。
生成元は shadcn/ui のボタンのクラス指定です。
// src/ui/components/button.tsx
"... [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
[&_svg:not([class*='size-'])]:size-4 は「自分の子孫の svg のうち、size- を含むクラスが付いていないものだけサイズを 4 にする」という Tailwind の書き方です。これが CSS へ展開される過程で svg:not(...) という形になり、Turbopack の CSS パーサーが解釈できずに落ちています。
アプリのバグではなく、Tailwind v4 が吐く CSS と Turbopack の CSS パーサーの相性 の問題です。
回避策 1: webpack で起動する
npx next dev --webpack
これで通りました。
▲ Next.js 16.3.1 (webpack)
✓ Ready in 422ms
webpack dev: GET /ja -> 200
<title>gem-hunter</title>
回避策 2: 本番ビルドで動かす
npm run build
npm start
本番ビルドはコンパイルに加えて型検査も通すぶん、npm run dev の起動より時間がかかります(といっても型検査を含めて 1 分はかかりません)。最後に出るルート一覧は、このアプリの全体像が 1 画面で分かるので目を通す価値があります。
Route (app)
┌ ○ /_not-found
├ ƒ /[locale]
├ ƒ /[locale]/gems
├ ƒ /[locale]/opengraph-image
├ ƒ /[locale]/repos/[owner]/[repo]
├ ƒ /api/auth/callback
├ ƒ /api/auth/login
├ ƒ /api/auth/logout
├ ƒ /api/search
└ ○ /icon.png
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand
○ は「ビルド時に作って固定できるページ」、ƒ は「リクエストのたびにサーバーで作るページ」です。検索画面も詳細画面も Gem 一覧(/[locale]/gems)も ƒ になっています。理由は第 11 回で扱います。
この節は 執筆時点(Next.js 16.3.1 / Tailwind CSS v4)の実測 です。どちらもバージョンが上がれば解消しうるので、あなたの手元で npm run dev が普通に動いたなら、それが正しい状態です。
E2E テストを 1 本だけ動かす
後半(第 16 回)で扱う E2E テストも、この時点で 1 本だけ動かせます。
npx playwright test e2e/sp-1.spec.ts
Running 1 test using 1 worker
✓ 1 [chromium] › e2e/sp-1.spec.ts:10:5 › SP-1: 検索して一覧が出る
1 passed
ここでも 外部ネットワークに出ていません。playwright.config.ts の設定により、テスト実行の前に 2 つのプロセスが自動で立ち上がります。
-
GitHub API のふりをするスタブサーバー(
node e2e/stub/server.mjs・ポート 8788) -
アプリ本体(
npm run build && npm start -- --port 3100)
そのうえで、アプリが見に行く GitHub API の URL をスタブに差し替えた状態で起動しています。だから API キーが要らず、GitHub が落ちていてもテストは通ります。
全部の E2E を流すのは、この回ではやめておきましょう。 検証時(2026-08-23)は 10 件が赤くなりましたが、原因はあなたの環境ではありません。 npm run build では作られない配信用ディレクトリを、テスト対象のアプリが読みにいくためです。引数なしの npx playwright test は 23 ファイル・107 テストを直列に実行し、初回はアプリの本番ビルドも挟みます。これは「環境で読めなかったことをスキップで隠さず、はっきり落とす」と決められている赤 で、判定のしかたは第 17 回で扱います。この回の目的は「動くことの確認」なので、1 本で十分です。
ディレクトリを覗く
gem-hunter/
├── app/ … 画面と API のルート(Next.js のファイル規約)
├── src/
│ ├── domain/ … このアプリ固有の「概念」。外部に依存しない
│ ├── usecases/ … 「検索する」「詳細を見る」といった操作の段取り
│ ├── infrastructure/ … 外の世界(GitHub API・Cloudflare)と話す部分
│ ├── composition/ … 上の 3 つを組み立てて配線する場所
│ ├── ui/ … 画面部品
│ └── shared/ … どこからでも使える小物
├── e2e/ … Playwright の E2E テストとスタブサーバー
├── tools/ … CI・検査スクリプト(Python / シェル)
├── docs/ … 要件定義・設計・ADR(設計判断の記録)
├── messages/ … 日本語・英語の表示文言
└── .github/ … GitHub Actions のワークフロー(Gem プールの定期更新・第 19 回)
テストファイルは __tests__/ のような専用ディレクトリにまとまっていません。実装ファイルの隣に置かれています(per-page.ts の隣に per-page.test.ts)。この配置には理由があり、第 13 回で扱います。
落とし穴
-
npm installではなくnpm ciを使う。package-lock.jsonに固定されたバージョンをそのまま入れるためです -
インストールスクリプトは既定でブロックされる。npm の新しい既定では、パッケージに同梱された install スクリプトが実行されません。gem-hunter は
package.jsonのallowScriptsでesbuild/msw/unrs-resolver/workerdの 4 つだけ明示的に許可しています -
npm run checkはこの段階で走らせない。「PR を出す前に全部のゲートを通す」ためのコマンドで、Lint・型・ユニットテスト・E2E・Lighthouse・アーキテクチャ検査などを一括で回します。第 19 回まで取っておきましょう -
npm run devが 500 を返しても、あなたの環境が壊れているわけではありません。npm testが通っているなら環境は正常です
まとめ
-
git clone→npm ci→npm testで、設定ゼロのまま 81 ファイル・954 件のユニット/結合テストが緑 になる。E2E はこの回ではe2e/sp-1.spec.tsの 1 本だけ(全部流すのは第 16 回、赤の判定は第 17 回) - 外部に出ないのは、ポートによる差し替え と MSW の未定義リクエスト禁止 という 2 つの仕組みによる
- 開発サーバーは執筆時点で Turbopack の CSS パースエラーに当たる。
next dev --webpackか本番ビルドで回避できる
シリーズの前後の記事
-
⬅️ 前の記事: 第 1 回 動いているリポジトリを読むという学び方
-
➡️ 次回予告: 第 3 回 要件IDとADR — 何を解くアプリなのかを地図にする
このアプリが何を解くのか、要件ID と ADR という「決定を残す」文化から読み解きます。
全 19 回をまとめて読みたい方は、同じ内容の Zenn Book(無料)へどうぞ。