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?

長文暗記アプリ「暗記ノート」開発記 #6 テスト・CI/CD構築編

1
Posted at

連載目次

回 内容
#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();
});
  1. フォルダ「面接用」と、そこに入ったテキストを保存しておく
  2. 画面を表示し、メニューを開いて「面接用を削除」を押す
  3. 保存データで、フォルダが消え、テキストが未分類(null)になったことを確かめる
  4. 画面でも「未分類」を選び、一覧にそのテキストが出ることを確かめる

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 のリポジトリとつなぐだけで、次のように動きます。

役割は次のように分けました。

  • 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 の最初の公開が失敗した原因を書きます。

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?