💨

Remix + Prisma + Vite + Vitestでテスト駆動開発

2024/02/23に公開

誰かと真剣に向き合いたい人のためのアプリ(仮)のクローズドβテストをしています。
Remixの情報がまだまだ少ないので誰かの手助けになればと思い、今回は開発の際の最小限のテンプレートを作ったので共有しておきます。
ここからTDDを簡単にはじめられるかと思います。
https://github.com/callmegema/sample_fullstack_remix

(事前登録してもらえると嬉しいです)
https://question-lh7ov7qpoa-an.a.run.app

install

Remix + Vite

今までunstableだったもののRemixでViteが正式リリースされたので入れてみます。

npx create-remix@latest --template remix-run/remix/templates/vite

npm run devで動作確認

https://remix.run/docs/en/main/future/vite#setup-vite
https://remix.run/blog/remix-vite-stable

Vitest

npm install -D vitest

package.jsonを編集

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

(個人的にBDDに影響を受けているのでspecも生やしておく)
app/check.spec.tsを作成してnpm run test or npm run specでVitestが動くか確認

// app/check.spec.ts
import { describe, it } from 'vitest'

describe("check", () => {
  it("show env without error", () => {
    console.log(`env: ${process.env.NODE_ENV}`);
  });
});

https://vitest.dev/guide/

Prisma

npm install prisma --save-dev
npx prisma init --datasource-provider mysql

prisma/schema.prismaに追記、idはCUIDに設定

// prisma/schema.prisma 

model Post {
  id        String     @id @default(cuid())
  title     String
  content   String?
  published Boolean @default(false)
}

https://www.prisma.io/docs/getting-started/quickstart

docker-compose.local.ymlを作成

開発用のDBとテスト用のDBを立ち上げます。

// docker-compose.local.yml
version: "3.4"
services:
  db:
    image: mysql:8.0.33
    platform: linux/amd64 # Apple Siliconのためplantformを指定
    environment:
      MYSQL_DATABASE: sample_development
      MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
    ports:
      - "3306:3306"
  db_test:
    image: mysql:8.0.33
    platform: linux/amd64 # Apple Siliconのためplantformを指定
    restart: always
    environment:
      MYSQL_DATABASE: sample_test
      MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
    ports:
      - "3307:3306"
    expose:
      - 3307
docker compose -f docker-compose.local.yml up

.envにDATABASE_URL="mysql://root:@localhost:3306/sample_development"を設定

dev用のdbのmigrate実行

npx prisma migrate dev --name init

test用のdbのmigrate実行

NODE_ENV=test DATABASE_URL=mysql://root:@localhost:3306/sample_test npx prisma migrate reset --force

vitest-environment-vprisma導入

vitest-environment-vprismaを入れることで、テスト毎にDBのレコードをクリーンアップできるようになります。

npm install -D vitest-environment-vprisma
// vitest.config.ts
import { defineConfig } from "vite";

export default defineConfig({
  test: {
    globals: true,  // Don't forget!
    environment: "vprisma",
    setupFiles: ["vitest-environment-vprisma/setup"]
  },
});
// tsconfig.json
{
  "compilerOptions": {
    "types": ["vitest/globals", "vitest-environment-vprisma"]
  }
}
//app/check.spec.ts
import { describe, it } from 'vitest'

describe("check", () => {
  it("show env without error", () => {
    console.log(`env: ${process.env.NODE_ENV}`);
  });

  const prisma = vPrisma.client;

  test("Add post", async () => {
    const post = await prisma.post.create({
      data: {
        title: "Hello World",
        content: "This is a post content",
      },
    });

    expect(
      await prisma.post.findFirst({
        where: {
          id: post.id,
        },
      })
    ).toStrictEqual(post);
    expect(await prisma.post.count()).toBe(1);
  });

  // Each test case is isolated in a transaction and also rolled back, so it is not affected by another test result.
  test("Count post", async () => {
    expect(await prisma.post.count()).toBe(0);
  });
});

テストが通るか確認

NODE_ENV=test DATABASE_URL=mysql://root:@localhost:3307/sample_test npm test

https://github.com/aiji42/vitest-environment-vprisma
https://quramy.medium.com/integrated-testing-with-prisma-4bc73404d027
https://zenn.dev/5t111111/scraps/f9002ee51a588a#comment-8d62ea1d307673

Faker

テストの並列実行のためにもFakerを入れておきます。

npm install --save-dev @faker-js/faker

実装

RemixのRoute File Namingでは、routesフォルダのフォルダにroute.tsxを置くことでコロケーションを実現できるようになっているため、以下のようなディレクトリ構成でコードとspecファイルを一緒に配置しています。個人的には同一フォルダ内にマークダウンで書いた仕様書も置いてしまうのが好みです。

PostのCRUDのディレクトリ構成

app/
├── routes/
│   ├── _index.tsx
│   ├── posts._index/
│   │   ├── route.tsx
│   │   ├── index.server.ts
│   │   └── index.server.spec.ts
│   ├── posts.$postId/
│   │   ├── route.tsx
│   │   ├── show.server.tsx
│   │   └── show.server.spec.ts
│   ├── posts.new/
│   │   ├── route.tsx
│   │   ├── create.server.tsx
│   │   └── create.server.spec.ts
│   ├── posts.$postId.edit/
│   │   ├── route.tsx
│   │   ├── edit.server.tsx
│   │   └── edit.server.spec.ts
│   ├── posts.$postId.delete/
│   │   ├── route.tsx
│   │   ├── delete.server.tsx
│   │   └── delete.server.spec.ts
│   ├── root.tsx
│   └── check.spec.ts
└── vite.config.ts

実装の一例

specファイル

// app/routes/posts._index/index.server.spec.ts
import { describe, it } from 'vitest'
import { faker } from '@faker-js/faker';

import { call } from "./index.server";

describe("index Posts", () => {
  const prisma = vPrisma.client;

  beforeEach(async () => {
    await prisma.post.create({data: {title: faker.lorem.words(5), content: faker.lorem.words(10)}})
    await prisma.post.create({data: {title: faker.lorem.words(5), content: faker.lorem.words(10)}})
    await prisma.post.create({data: {title: faker.lorem.words(5), content: faker.lorem.words(10)}})
  });

  it("index", async () => {
    const posts = await call()
    expect(posts.length).toEqual(3)
  })
});

route.tsxの実装

// app/routes/posts._index/route.tsx
import { json } from "@remix-run/node"
import { Link, useLoaderData } from "@remix-run/react"
import { call } from "./index.server"
import { Post } from "@prisma/client"

export const loader = async () => {
  const posts = await call()
  return json(posts)
}

export default function PostIndex() {
  const posts: Post[] = useLoaderData()
  return (
    <div>
      <h1>Posts</h1>
      <Link to="new">New</Link>
      <ul>
        {posts.map((post: Post) => (
          <li key={post.id}>
            <Link to={post.id}>{post.title}</Link>
          </li>
        ))}
      </ul>
    </div>
  )
}

サーバー側の実装

// app/routes/posts._index/index.server.ts
import { prisma } from "../../db.server";

export async function call() {
  return await prisma.post.findMany()
}

https://remix.run/docs/en/main/file-conventions/routes#folders-for-organization

余談

RemixでViteの安定版対応がリリースされたため早速入れてVitestでTDDをしてみたのですが、実際に動かしてみると不安定でテスト途中で落ちてしまうことがあります。
今後も改善がされていくと思うので都度Rmixをupdateしていこうと思います。
あるいは、Vitestを安定化させる方法など知っていたら教えていただけると有り難いです。

Discussion