${profile.profile} ${aboutPageLink}

もふもふ技術部

IT技術系mofmofメディア

React Router v7、Prisma、Vitestでテストを書き始める準備

Remixを触ってみたいと思っていたら、React Router v7に統合されてしまいました。 そんなReact Routerでプロジェクトを作成し、PrismaとVitestを導入してテストを書き始める準備について書きました。

React Routerでプロジェクト作成

プロジェクトの作成は以下を実行します。

> npx create-react-router@latest my-react-router-app
Need to install the following packages:
create-react-router@7.0.2
Ok to proceed? (y)

         create-react-router v7.0.2
      â—¼  Directory: Using my-react-router-app as project directory

      â—¼  Using default template See https://github.com/remix-run/react-router-templates for more
      ✔  Template copied

   git   Initialize a new git repository?
         Yes

  deps   Install dependencies with npm?
         No
      â—¼  Skipping install step. Remember to install dependencies after setup with npm install.

      ✔  Git initialized

  done   That's it!

         Enter your project directory using cd ./my-react-router-app
         Check out README.md for development and deploy instructions.

         Join the community at https://rmx.as/discord

docker composeでDBを用意

今回はPostgreSQLを使用します。

# compose.yaml
services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:
    driver: local

Prismaå°Žå…¥

> pnpm i -D prisma
> npx prisma init --datasource-provider postgresql

スキーマ定義

デフォルトではPrismaのスキーマで定義されたモデル名がそのままテーブル名となり作成されますが、 あまり馴染みがないので小文字の複数形で作成されるように@@mapでテーブル名を指定します。

www.prisma.io

// prisma/shema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  @@map("users")
  id    Int    @id @default(autoincrement())
  email String @unique
  name  String
  posts Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Post {
  @@map("posts")
  id Int @id @default(autoincrement())
  title String @db.VarChar(255)
  content String @db.VarChar(255)
  published Boolean @default(false)
  user User @relation(fields: [userId], references: [id])
  userId Int
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

開発用とテスト用のDBをそれぞれ作成

開発用とテスト用でDBを分けたいので、dotenvで読み込む環境変数を切り替える方法をとります。

> pnpm i -D dotenv-cli

.env.development、.env.testを作成し、それぞれに環境変数DATABASE_URLを定義しておきます。

# .env.development
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema

# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings

DATABASE_URL="postgresql://postgres:password@localhost:5432/myapp_development"
# .env.test
DATABASE_URL="postgresql://postgres:password@localhost:5432/myapp_test"

package.jsonのscriptにDBへのスキーマ反映、マイグレーションコマンドを追加します。 dotenv-cliにより読み込む環境変数を指定して実行先を切り替えるようにしています。

追加内容

  • スキーマ変更後、Prisma Clientの更新・型生成を行うためのnpx prisma generateコマンドを指定
  • マイグレーション実行・ファイル生成を行うnpx prisma migrate devコマンドを指定
    • 初回DB作成を行う際はinitオプションをつけて実行する
    • テスト環境DBに対しては本番環境と同様にマイグレーションファイル反映のみ行いたいのでnpx prisma migrate deployコマンドを使用
// package.json
{
  "name": "my-react-router-app",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "react-router build",
    "dev": "react-router dev",
    "start": "react-router-serve ./build/server/index.js",
    "typecheck": "react-router typegen && tsc --build --noEmit",
+    "prisma": "npx prisma generate",
+    "db:migrate": "dotenv -e .env.development -- npx prisma migrate dev",
+    "db:migrate:test": "dotenv -e .env.test -- npx prisma migrate deploy",
  },
  ...
}

Seeding

prismaフォルダにseed.tsファイルを追加し、seedを定義します。

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

async function main() {
  const alice = await prisma.user.upsert({
    where: { email: '[email protected]' },
    update: {},
    create: {
      email: '[email protected]',
      name: 'Alice',
      posts: {
        create: {
          title: 'Check out Prisma with Next.js',
          content: 'https://www.prisma.io/nextjs',
          published: true,
        },
      },
    },
  });
  const bob = await prisma.user.upsert({
    where: { email: '[email protected]' },
    update: {},
    create: {
      email: '[email protected]',
      name: 'Bob',
      posts: {
        create: [
          {
            title: 'Follow Prisma on Twitter',
            content: 'https://twitter.com/prisma',
            published: true,
          },
          {
            title: 'Follow Nexus on Twitter',
            content: 'https://twitter.com/nexusgql',
            published: true,
          },
        ],
      },
    },
  });
  console.log({ alice, bob });
}
main()
  .then(async () => {
    await prisma.$disconnect();
  })
  .catch(async (e) => {
    console.error(e);
    await prisma.$disconnect();
    process.exit(1);
  });

.tsファイル実行のために別途tsxをインストールしておきます。 (ドキュメントではts-nodeを使用していますが、.tsファイルの拡張子がうまく認識されずエラーになる)

github.com

// package.json
{
  "name": "my-react-router-app",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "react-router build",
    "dev": "react-router dev",
    "start": "react-router-serve ./build/server/index.js",
    "typecheck": "react-router typegen && tsc --build --noEmit",
    "prisma": "npx prisma generate",
    "db:migrate": "dotenv -e .env.development -- npx prisma migrate dev",
    "db:migrate:test": "dotenv -e .env.test -- npx prisma migrate deploy",
+    "db:seed": "dotenv -e .env.development -- npx prisma db seed",
+    "db:seed:test": "dotenv -e .env.test -- npx prisma db seed"
  },
+  "prisma": {
+    "seed": "tsx prisma/seed.ts"
+  },
  ...
}

npm run db:seedで実行するかmigrate実行時にseedが実行されます。

Vitestå°Žå…¥

Vitestと併せてjsdomやtesting-library各種をインストールします。

> pnpm i -D vitest jsdom @testing-library/react @testing-library/user-event @testing-library/jest-dom @testing-library/react-hooks

vite.config.tsの設定

React RouterではすでにViteが使用されているのでvite.config.tsへ設定を行います。 @testing-library/jest-domのカスタムマッチャーを使用するために別途設定ファイルを作成してsetupFilesへパスを指定します。

また、ReactRouterのViteプラグインはテストでの使用が想定されていないため(開発サーバーと本番ビルドでの使用のみの想定)、Vitestによるテスト実行時は使用されないように環境変数VITESTで制御します。 React Routerのドキュメントの方で見つけられなかったので、Remixのドキュメントを詳しくは参照してください。 remix.run

// vite.config.ts
+/// <reference types="vitest/config" />
import { reactRouter } from '@react-router/dev/vite';
import autoprefixer from 'autoprefixer';
import tailwindcss from 'tailwindcss';
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig({
  css: {
    postcss: {
      plugins: [tailwindcss, autoprefixer],
    },
  },
+  plugins: [!process.env.VITEST && reactRouter(), tsconfigPaths()],
+  test: {
+    globals: true,
+    environment: 'jsdom',
+    setupFiles: ['./tests/setup.ts'],
+  },
});

tsconfig.jsonの設定

JestのようなグローバルAPIとして使用するための設定もしておきます。 describe等のimportが省略できるようになって楽です。

// tsconfig.json
{
  ...
  "compilerOptions": {
    "lib": ["DOM", "DOM.Iterable", "ES2022"],
+    "types": ["node", "vite/client", "vitest/globals"],
    ...
}

最後にpackage.jsonのscriptsへテストのコマンドを追加します。

// package.json
{  
  ...
  "scripts": { 
    ...
+    "test": "vitest"
  },

テストを書いてみる

最後にプロジェクト作成時に生成されるコンポーネントに対して簡単なテストを書いてみます。 react-routerが提供するcreateRoutesStubを使用することでコンポーネントをルーティングに依存せず簡易的にモックしてテストが可能です。

reactrouter.com

import { createRoutesStub } from 'react-router';
import { render, screen } from '@testing-library/react';
import Home from '~/routes/home';

describe('React Router initial page', () => {
  it('renders the home page and navigation links', async () => {
    const Stub = createRoutesStub([{ path: '/', Component: Home }]);
    render(<Stub />);

    const logo = screen.getAllByRole('img', { name: /react router/i }).at(0);
    expect(logo).toBeInTheDocument();

    await waitFor(() => {
      screen.findByText('React Router Docs');
      screen.findByText('Join Discord');
    });
  });
});

さいごに

React Router、Prisma、Vitestによるテスト環境の構築を行いました。なにかしら参考になると嬉しいです。

ここまで読んでいただきありがとうございました。