連載目次
| 回 | 内容 |
|---|---|
| #1 | 企画・課題発見 |
| #2 | 要件定義 |
| #3 | 設計 |
| #4 | データ処理・ロジック実装編 |
| #5 | UI・画面実装編 |
| #6 | テスト・CI/CD構築編(この記事) |
| #7 | 開発中に遭遇した技術的問題 |
| #8 | 完成・振り返り |
はじめに
前回までで、データ処理(lib)と画面(components)を実装しました。
今回は、それらが正しく動くことを確かめるテストと、コードを GitHub に送るたびに自動でチェックし、公開まで自動で行う仕組み(CI/CD)を書きます。
全体像
| 役割 | 使ったもの | 何をするか |
|---|---|---|
| テスト | Vitest / React Testing Library | 関数や画面の部品が期待どおり動くかを確かめる |
| コードの書き方チェック | ESLint | 書き方の誤りや、React のルール違反を見つける |
| CI(自動チェック) | GitHub Actions | push や PR のたびに、Lint・テスト・ビルドを自動で実行する |
| CD(自動公開) | Vercel |
main に入ったコードを自動で本番に公開する |
テスト基盤は最初に入れる
テストの仕組み(Vitest)は、アプリを作り始める最初の Issue(#1 環境構築)で入れました。後から入れようとすると、テストを書かないまま機能が増えていき、「今動いているものを壊していないか」を確かめる手段がないまま開発が進んでしまうためです。
環境構築の完了条件は、次の3つが手元で通ることにしました。
-
npm run build(本番用にビルドできる) -
npm run test(サンプルのテストが通る) -
npm run lint(Lint でエラーが出ない)
途中で、依存パッケージの組み合わせでつまずきました。テスト用の @testing-library/jest-dom が求める Vitest のバージョンと、Next.js の雛形に最初から入っていた @types/node のバージョンがかみ合わず、インストールの段階でエラーになりました。原因が @types/node の古さにあることを確かめ、最新版に更新して解消しました。
Vitest の設定
// vitest.config.ts
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./vitest.setup.ts"],
},
resolve: {
alias: {
"@": path.resolve(__dirname, "."),
},
},
});
-
environment: "jsdom":ブラウザの代わりになる環境でテストを動かします。画面の部品や localStorage を、本物のブラウザなしで試せます -
alias:アプリ本体と同じく、@/lib/storageのような書き方で読み込めるようにしています
// vitest.setup.ts
import "@testing-library/jest-dom/vitest";
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn(), /* ほか省略 */ }),
}));
Next.js のページ移動(useRouter)は、テストの環境ではそのまま動きません。そこで、偽物(モック)に置き換えています。「はじめる」を押したら練習画面へ移動する、といった動きは、この偽物の push が呼ばれたかどうかで確かめます。
テストの書き方
テストは6ファイル・67件です(2026年9月時点)。
| ファイル | 件数 | 主に確かめていること |
|---|---|---|
lib/textProcessing.test.ts |
18 | 文の分け方、ブロックの等分、タイトルの自動作成、キーワードの抽出 |
lib/storage.test.ts |
6 | 保存・読み出し、壊れたデータのとき空の配列を返すこと |
components/home-screen.test.tsx |
16 | 登録、一覧の開閉、絞り込み、フォルダの作成・削除、テキストの削除 |
components/practice-screen.test.tsx |
12 | 3モードの切り替え、隠す/表示、習熟度・ブックマークの保存、前後移動 |
components/menu-drawer.test.tsx |
9 | メニューの操作で、正しい関数が呼ばれること |
components/landing-screen.test.tsx |
6 | トップページのデモが動くこと |
画面のテストは「使う人の目線」で書く
画面の部品のテストには、React Testing Library を使いました。部品の中身(state の値など)を直接のぞくのではなく、画面に見えている文字やボタンの名前で要素を探し、実際に操作して結果を確かめる のが特徴です。
例えば、フォルダを削除したときのテストはこうなっています。
it("フォルダを削除すると、中にあったテキストは未分類に戻る", () => {
saveFolders([{ id: "f1", name: "面接用" }]);
saveTexts([{ id: "t1", title: "面接原稿", folderId: "f1", /* ほか省略 */ }]);
render(<HomeScreen />);
fireEvent.click(screen.getByLabelText("メニューを開く"));
fireEvent.click(screen.getByLabelText("面接用を削除"));
expect(loadFolders()).toEqual([]);
expect(loadTexts()[0].folderId).toBeNull();
fireEvent.click(screen.getByRole("radio", { name: "未分類" }));
fireEvent.click(screen.getByText(/保存済みテキスト/));
expect(screen.getByText("面接原稿")).toBeInTheDocument();
});
- フォルダ「面接用」と、そこに入ったテキストを保存しておく
- 画面を表示し、メニューを開いて「面接用を削除」を押す
- 保存データで、フォルダが消え、テキストが未分類(
null)になったことを確かめる - 画面でも「未分類」を選び、一覧にそのテキストが出ることを確かめる
getByLabelText("面接用を削除") のように、画面の読み上げ用の名前(aria-label)でボタンを探しています。アイコンだけのボタンでも名前を付けておく必要があるので、テストを書くこと自体が、画面の読み上げ対応の確認にもなりました。
テスト同士が影響し合わないようにする
beforeEach(() => {
localStorage.clear();
});
localStorage は、テストの間で中身が残ります。前のテストで保存したデータが次のテストの結果を変えてしまわないよう、各テストの前に空にしています。
CI:GitHub Actions で自動チェック
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Test
run: npm run test
- name: Build
run: npm run build
main と develop への push と、この2つに向けた PR のたびに、Lint → テスト → ビルドの順で自動実行します。どれか1つでも失敗すると PR に赤い印が付くので、壊れたコードをマージする前に気づけます。
-
npm ci:package-lock.jsonに書かれたとおりのバージョンを入れるコマンドです。手元と同じ組み合わせで確かめるために使っています -
cache: "npm":一度ダウンロードしたパッケージを使い回して、実行時間を短くします
手元では通るのに、CIで失敗した
CIを入れた直後、手元では通るテストが、CI上ではほとんど失敗しました。原因は Node.js のバージョンの違い でした。
- 手元:新しいバージョンの Node.js
- CI:最初は Node.js 20 を指定していた
テストで使う jsdom が必要とする Node.js の機能が、20 には入っていませんでした。CIで使うバージョンを新しいもの(24)に上げて解消しました。「手元で動く」ことと「どの環境でも動く」ことは別だ、と分かった場面です。
main にも CI を入れる
CIの設定は、最初は develop だけに入っていました。しかし、公開中の版で緊急の修正をするときは、main から hotfix/* ブランチを作って直します。そのときにも自動チェックが効くよう、main にも同じ設定を入れました(PR #28)。
CD:Vercel で自動公開
公開には Vercel の無料プラン(Hobby)を使いました。GitHub のリポジトリとつなぐだけで、次のように動きます。
-
mainにマージする → 本番(https://anki-note.vercel.app)に自動で公開される - PR を作る → その PR 専用の確認用URL(プレビュー)が自動で作られる
役割は次のように分けました。
- GitHub Actions:コードの品質チェックだけを担当する
- Vercel:本番に公開することだけを担当する
GitHub Actions から Vercel に公開の指示を出す形にすると、Vercel の認証情報(トークン)を GitHub 側に登録して管理する必要が出てきます。役割を分けておけば、そうした秘密の情報をやり取りせずに済みます。
実は、Vercel の最初の公開は失敗しました。原因は Vercel の設定ではなく、ブランチの運用にありました。この話は次回(#7)で書きます。
まとめ
- テストの仕組みは最初の Issue で入れ、ビルド・テスト・Lint が通ることを環境構築の完了条件にした
- 画面のテストは React Testing Library で、画面に見える名前でボタンを探して操作する形で書いた
- GitHub Actions で、
main・developへの push と PR のたびに Lint・テスト・ビルドを自動実行した - CI と手元の Node.js のバージョンの違いで失敗し、バージョンを合わせて解消した
- 公開は Vercel に任せ、CI(品質チェック)と CD(公開)の役割を分けた
次回は「開発中に遭遇した技術的問題」です。ブロック分割機能を廃止するまでの経緯と、Vercel の最初の公開が失敗した原因を書きます。