テスト前提で設計したWebアプリのハンズオン - 読書管理アプリ その1
なぜこの記事を書いたか 動くものをまず作って、そこにテストを後付けしようとした。しかし、できなかった。 具体的にはこういう壁にぶつかった。 ModelなどのDB接続の切り替えがうまくいかない Controllerをテストしようにもいろいろおかしくなる 悩んだ結果、気づいたことがある。そもそもそんなものは単体テストじゃない。 ロジックをControllerやModelに全部書いていたのが悪かった。 「じゃあ最初からそう書けばいいじゃないか」という話だが、それが難しい。雑魚プログラマにいきなりクリーンな設計はできない。 なので発想を逆にした。テスト前提でコードを書く。 テストが書けない場所にはロジックを書かない。それを体で覚えるために、今回のハンズオンを始めた。 方針 生成AIに実装ヒントと次のステップを教えてもらいながら、コードは自分で書く。 正直、コード自体はAIに書かせてもいいと思っている。ただ、設計の考え方は頭に入れる必要があるので、写経しながら理解を深めている。 テストする場所の原則はシンプルだ。 フレームワークで用意された便利クラスはテストしない DBやAPIなど外界と接する場所はテストしない 自分で書いた純粋なTS部分だけをテストする 作るもの 読書レビューサイト。Todoアプリはビジネスロジックがほぼ存在しないのでテストの練習に向いていない。読書レビューサイトなら本の状態遷移(積読→読中→読了)などのルールが自然に生まれるので、テストの旨味がある。 技術スタック Next.js SQLite + Prisma vitest ディレクトリ構成 src/ domain/ entity/ valueobject/ policy/ service/ repository/ この構成の考え方は以下の通り。 層 役割 entity 概念そのもの(Book, User) valueobject 値の制約を持つクラス(ISBN, Rating) policy ビジネスルールを切り出したクラス service 複数クラスをまたぐフロー repository DBとのアダプタ(副作用をここに閉じ込める) repositoryはCI4でいうModelに近い立ち位置だ。 ただし決定的な違いがある。CI4のModelはActiveRecordパターンでクラス自身がDBを知っているため切り離せないが、repositoryはInterfaceと実装を分けることで差し替え可能にする。これがDI(依存性の注入)の肝で、テスト時にMockに差し替えられる。 vitestのセットアップ npm install -D vitest @vitejs/plugin-react vite-tsconfig-paths npm install -D @testing-library/react @testing-library/jest-dom vitest.config.ts import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' import tsconfigPaths from 'vite-tsconfig-paths' export default defineConfig({ plugins: [react(), tsconfigPaths()], test: { environment: 'jsdom', globals: true, setupFiles: ['./src/test/setup.ts'], }, }) package.jsonのscriptsに追加。 ...