テスト¶
AdaptiveLearnerのテスト規律は、すべての変更に対してmake testによって強制されます。戦略はピラミッド形式です。ベースにユニットテスト、中間に統合テスト、頂点にE2Eスモークテストが位置します。
テスト数¶
| レイヤー | ツール |
|---|---|
| バックエンドユニット + 統合 | pytest ^9 |
| プラグインテスト(13プラグイン) | pytest ^9 |
| フロントエンドユニット + 統合 | Vitest 4 |
| E2Eスモーク | Playwright |
| Dexieモードのリリースゲート | Playwright |
テスト数はリリースごとに増えていきます。同期がずれてしまう数値の重複を避けるため、このページには合計をハードコードしません。テスト数とカバレッジの唯一の正準かつ常に最新のソースはdocs/audits/current-coverage.mdです。13のプラグインは、assessment、3つのAIプロバイダー(anthropic / openai / gemini)、session、tracking、tools、gamification、anki、notebooklm、learning-repo、content-loader、missionsです。
バックエンドpytest¶
make test-backend # 786テスト、約35秒
cd backend && poetry run pytest -k "test_session" -v
cd backend && poetry run pytest --pdb
テストはbackend/tests/に格納されています。conftest.pyのフィクスチャが、テストごとにフレッシュなインメモリSQLite DB、TestClient、モック化されたプラグインマネージャーを提供します。テスト分離は厳格で、app.*のインポート前にADAPTIVE_LEARNER_TEST=1が設定されます。
プラグインテスト¶
各プラグインは独自のtests/ディレクトリを持ちます。
make test-plugins # すべて
make test-plugin-session # 1つだけ
cd plugins/adaptive-learner-plugin-session && poetry run pytest
プラグインテストはFastAPIアプリをロードしません。プラグインのモジュールを単独でテストします。フック発火をテストする際はpluggy.PluginManagerをモックしてください。
フロントエンドVitest¶
make test-frontend # 387テスト、約2秒
cd frontend && bunx vitest # watchモード
cd frontend && bunx vitest run src/storage/ # 1つのディレクトリ
テストはソースの隣に配置されます: Component.tsxの隣にComponent.test.tsx。環境はhappy-dom; React 19 + RTL。
モックパターン¶
AIプロバイダー: global.fetchをモックし、URL、ヘッダー、ボディをアサートします。
beforeEach(() => {
global.fetch = vi.fn(async (input, init) => {
calls.push({url, method, body});
return new Response(JSON.stringify({content: [{type: "text", text: "hi"}]}), {status: 200});
});
});
fake-indexeddb: すべてのDexieテストファイルの先頭に記述します。
import "fake-indexeddb/auto";
beforeEach(async () => {
await _resetDbForTests();
const {IDBFactory} = await import("fake-indexeddb");
(globalThis as unknown as {indexedDB: IDBFactory}).indexedDB = new IDBFactory();
});
各テストはフレッシュなインメモリIndexedDBを取得します。リークはありません。
api/client.tsのモック(レガシーページ):
vi.mock("../api/client", async () => {
const actual = await vi.importActual<typeof import("../api/client")>("../api/client");
return {...actual, api: {...actual.api, users: {...actual.api.users, get: apiGetMock}}};
});
ページはgetStorage()をインポートし、それがApiStorageに委譲し、さらにapi.*に委譲します。モックはapi.*レイヤーで割り込み、ストレージスタックを通じて発火します。
Playwright E2E¶
cd e2e && npx playwright test
cd e2e && npx playwright test --ui # インタラクティブ
cd e2e && npx playwright test smoke/mobile-viewports.spec.ts
スモークスペックは重要なユーザーパスをカバーしています。
- ランディングの言語ピッカー + オンボーディングフォーム
- アセスメント12問 + レーダーレンダリング
- セッション開始 + 終了 + レーティング
- Settings言語 + APIキー
- カリキュラム作成
- モバイルビューポート(iPhone SE、iPhone 14、Pixel 7、iPad)
スペックはdata-testidセレクターのみを使用します。壊れやすいCSSセレクターは使用しません。スモークスペックはmake testのパスには含まれていません。実行中のアプリが必要です(先にmake dev-bgを実行)。
カバレッジ¶
カバレッジはmainへのすべてのプッシュについてCIで実行されます。アーティファクトをダウンロードするには:
.claude/rules/quality-checks.mdのターゲット:
- サービス + ビジネスロジック: 最低95%
- APIエンドポイント: 最低90%
- ロジックを持つフロントエンドコンポーネント: 最低85%
- フック + ユーティリティ: 最低95%
全体: プロジェクト全体で85〜95%。
pre-commit¶
フック: ruff check(自動修正)、ruff format、末尾の空白、ファイル末尾の修正、check-yaml、check-merge-conflict。バックエンドのみ。フロントエンドのリントはpre-commitではなく、CI時に実行されます。
CI¶
CIは2つの層に分かれます。正しさのゲートはすべてのPRで実行され(マージには合格が必須です)、コストの高いスイートや警告のみのスイートはナイトシフトとリリース時に実行されます。
.github/workflows/ci.ymlはdevelop / mainへのプッシュとすべてのPRで実行されます(Python 3.12)。
- バックエンドテスト(pytest)
- プラグインテスト(
make test-plugins、バックエンドのvenv経由で13個すべて) - フロントエンド:
tsc --noEmit、ESLint(--max-warnings 0)、循環依存チェック、Stylelint、Vitest、vite build、npm audit - すべてのファイルに対するpre-commitフック
- バックエンドのruff + mypy + pip-audit
- ドキュメントのドリフト検証(
verify_docs.py+ mkdocs-navの同期)
Test Impact Analysis (#615): PRでは影響を受けるテストのみが実行されます - vitest run --changed origin/<base>とpytest --testmon。develop / mainへのプッシュ、ナイトリー実行、リリース実行では常にフルスイートが実行されます。フルスイートへのフォールバックは自動です(ベース参照を解決できない場合、またはtestmonのキャッシュミス)。
さらに、いくつかのPRゲートは独自のワークフローにあります。
complexity-check.yml- 複雑度ラチェットゲート(make check-complexity-gate、Pythonにはradon、TSにはESLintの複雑度ルール)。これはベースラインラチェットです。.complexity-baselineに対して新規または悪化した違反がある場合にのみ失敗するため、既存の負債の一掃を強制することなく、新しい複雑度をブロックします。警告のみの完全な複雑度レポートはナイトリーで実行されます。cohesion-check.yml- ファイルサイズガード(.filesize-whitelistに対するゲート)に加えて、2つのクラス名ゲート: 死んだCSSクラス名(.dead-classnames-baselineに対するcheck-dead-classnames.py)と未スタイルclassNameゲート(--unstyled、.unstyled-classnames-baselineに対するラチェット) - トークンがすべて死んでいるclassNameはPRをブロックします。対になるフォルダーサイズガードはローカルでmake check-folder-sizeにより実行します。visual-baseline-gate.yml- 視覚的にクリティカルなパス(レッスンコンポーネント、演習レンダラー、テーマ/CSSファイル)を変更するPRは、影響を受けるベースラインスクリーンショットを同じPRで持ち込まなければなりません。証明可能に無害な変更にはエスケープラベルvisual-baselines-unaffectedを使います。testid-reference-gate.yml- E2Eスペックが静的に参照するdata-testidを(ユーザーの目に付きやすいサーフェスで)、スペックに触れずに削除または改名するPRは、このゲートで失敗します(make check-testid-refs)。エスケープラベルはtestid-refs-unaffected。docker-build-smoke.yml- 本番Composeイメージ(ランチャー/install.shのパス)のビルドのみのスモーク。PRではパスフィルター付き、加えてrelease/**、週次、手動ディスパッチで実行。ローカルではmake docker-build-smoke。
ナイトシフト / リリース(PRでは実行されない):
dexie-smoke.yml- DexieモードのE2Eゲート(毎日 +release/**+ 手動ディスパッチ。ローカルではmake test-dexie-smoke)coverage.yml- カバレッジレポート(毎日 + 手動ディスパッチ)security-scan.yml- pip-audit / npm audit / bandit(週次 +release/**+ 手動ディスパッチ。警告のみ)content-stats.yml- フレッシュなコンテンツリポジトリのチェックアウトに対するコンテンツ統計ドリフトの検査(毎日 + 手動ディスパッチ)mutation-frontend.yml- Strykerミューテーションテスト(リポジトリ変数ENABLE_NIGHTLY_MUTATIONの背後でのナイトリー + 手動ディスパッチ。実行がジョブのタイムアウトに収まるよう、1回の実行でファイルの1スライスをミューテートします)。バックエンドのミューテーションテストはmutmutを使用しますwebkit-gate.yml- 実WebKitエンジンのレイアウトゲート(Chromiumのゲートには構造的に見えないiOS/Safariのバグクラス)。リポジトリ変数ENABLE_NIGHTLY_WEBKITの背後で毎日、release/**では常に、手動ディスパッチでも実行されますvisual-regression.yml- ビジュアルベースラインのマトリクス(毎日 + 手動ディスパッチ。update_baselines=trueはベースラインをCIで再レンダリングし、アーティファクトとしてアップロードします)visual-baseline-sync.yml- サービスワークフロー: ベースラインをCIでレンダリングし、コミットとしてPRブランチにプッシュします(ラベルrefresh-visual-baselines、またはPR番号を指定した手動ディスパッチ) - マージ前の画像レビューは引き続き必須です
.github/workflows/release-gate.ymlはタグプッシュ時に実行されます。バージョンピンがバージョンを持つすべてのファイルにわたって同期されていること(ドリフトなし)、プラグインのロックファイルが一致していること、再生成されるアーティファクトが最新であることを検証します。