Remix + Prisma + Vite + Vitestでテスト駆動開発
誰かと真剣に向き合いたい人のためのアプリ(仮)のクローズドβテストをしています。
Remixの情報がまだまだ少ないので誰かの手助けになればと思い、今回は開発の際の最小限のテンプレートを作ったので共有しておきます。
ここからTDDを簡単にはじめられるかと思います。
(事前登録してもらえると嬉しいです)
install
Remix + Vite
今までunstableだったもののRemixでViteが正式リリースされたので入れてみます。
npx create-remix@latest --template remix-run/remix/templates/vite
npm run devで動作確認
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}`);
});
});
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)
}
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
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()
}
余談
RemixでViteの安定版対応がリリースされたため早速入れてVitestでTDDをしてみたのですが、実際に動かしてみると不安定でテスト途中で落ちてしまうことがあります。
今後も改善がされていくと思うので都度Rmixをupdateしていこうと思います。
あるいは、Vitestを安定化させる方法など知っていたら教えていただけると有り難いです。
Discussion