SQLite3 + Prisma の環境に Vitest で Unit Test を書く
スタック
- SQLite3 : DB のテストをしたいが環境を用意するのは面倒なのでファイルベースで動くこれを使う。
- Prisma : Node.js の ORM ライブラリを使って Unit test をした場合どうなるか確認する。
yarn prisma studio
で DB の確認ができるのありがたい。 - Vitest : Jest 互換の API で書ける unit test framework 。実行が早い。今回はモックを使わず実際にデータを投入するテストを行う。
初期設定
TypeScript のスクリプトを実行できる環境を作っていく。実行には ts-node
ではなく esbuild-register
を使ってより早く実行できるようにする。
まずはテストを含めず、単純にチュートリアルレベルのことを実行できるようにする。
$ mkdir prisma-sqlite3
$ cd prisma-sqlite3
$ yarn init -y
$ yarn add -D prisma typescript esbuild esbuild-register @types/node
$ touch index.ts
package.json
の scripts
は下記のように設定する。
"scripts": {
"start": "node -r esbuild-register index.ts",
},
tsconfig.json
も設定する。チュートリアルと同じ。
{
"compilerOptions": {
"sourceMap": true,
"outDir": "dist",
"strict": true,
"lib": ["esnext"],
"esModuleInterop": true
}
}
prisma の初期設定コマンドを実行する。 prisma
ディレクトリが作成され、配下に設定ファイルが配置される。
$ yarn prisma init
生成された設定ファイルを SQLite3 用に書き換える。SQLite3 のデータファイルのパスを定義しているが、実際に作る必要はない。
DATABASE_URL="file:./dev.db"
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model Post {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// SQLite では @db.VarChar(255) はサポートされていないので外す
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
}
model Profile {
id Int @id @default(autoincrement())
bio String?
user User @relation(fields: [userId], references: [id])
userId Int @unique
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
profile Profile?
}
scheme の定義が完了したら下記を実行して DB にテーブルを作成する。
$ yarn prisma migrate dev --name init
yarn prisma studio
を実行するとブラウザで作成したテーブルを確認できる。
実行確認
データを扱う Prisma クライアントライブラリを入れてスクリプトを記述する。
$ yarn add @prisma/client
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function main() {
await prisma.user.create({
data: {
name: "Alice",
email: "alice@prisma.io",
posts: {
create: { title: "Hello World" },
},
profile: {
create: { bio: "I like turtles" },
},
},
});
const allUsers = await prisma.user.findMany({
include: {
posts: true,
profile: true,
},
});
console.dir(allUsers, { depth: null });
}
main()
.catch((e) => {
throw e;
})
.finally(async () => {
await prisma.$disconnect();
});
ひとまず実行してみる。yarn start
でデータ投入結果を表示できる。
テストを書く
ここからが本題。DB の unit test を定義していく。カバレッジも取得するので c8
も導入する。
$ yarn add -D vitest c8
$ touch index.test.ts
テストを書く前に package.json
にテスト実行用のスクリプトを記述しておく。今回は DB に実際に投入するのでテスト実行前にテーブルをリセットする prisma migrate reset -f
を実行する。
"scripts": {
"start": "node -r esbuild-register index.ts",
"test": "vitest run",
"coverage": "vitest run --coverage",
"db:reset": "prisma migrate reset -f",
"db:test": "yarn db:reset && yarn test",
"db:test:coverage": "yarn db:reset && yarn coverage"
},
index.test.ts
テストを書いていく。この際、ひとまず愚直でもいいので最初の 1 回を通す。関数化などはその後に行う。
import { test, expect } from "vitest";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
test("User does not exist in the initial DB", async () => {
const users = await prisma.user.findMany({
include: {
posts: true,
profile: true,
},
});
expect(users).toEqual([]);
});
テストを記述して実行。失敗する場合はテーブルの状態を確認する。
$ yarn db:test
成功したら関数として切り出していく。
export async function findAllUser() {
const users = await prisma.user.findMany({
include: {
posts: true,
profile: true,
},
});
return users;
}
test("User does not exist in the initial DB", async () => {
const users = await findAllUser();
expect(users).toEqual([]);
});
最終的に index.ts
に移し、そこから読み込むようにする。後はこれを繰り返してテストパターンを増やしていく。
import { findAllUser } from "./index";
test("User does not exist in the initial DB", async () => {
const users = await findAllUser();
expect(users).toEqual([]);
});
テストケースを増やす
index.ts
にて基本的な CRUD パターンを実装する。後述しますが、 deleteUserById()
は失敗します。
import { PrismaClient, Prisma } from "@prisma/client";
const prisma = new PrismaClient();
export async function createUser(user: Prisma.UserCreateInput) {
await prisma.user.create({ data: user });
}
export async function findAllUser() {
const user = await prisma.user.findMany({
include: {
posts: true,
profile: true,
},
});
return user;
}
export async function findUserById(id: number) {
const user = await prisma.user.findFirst({
where: { id },
include: {
posts: true,
profile: true,
},
});
return user;
}
export async function updateUserById(id: number, user: Prisma.UserUpdateInput) {
await prisma.user.update({
where: { id },
data: user,
});
}
export async function deleteUserById(id: number) {
await prisma.user.delete({ where: { id } });
}
export async function disconnectDB() {
await prisma.$disconnect();
}
index.test.ts
にもテストを書いてく。
import { describe, expect, test, afterAll } from "vitest";
import { Prisma } from "@prisma/client";
import {
createUser,
deleteUserById,
disconnectDB,
findAllUser,
findUserById,
updateUserById,
} from "./index";
const demoUser: Prisma.UserCreateInput = {
name: "Alice",
email: "alice@prisma.io",
posts: {
create: { title: "Hello World" },
},
profile: {
create: { bio: "I like turtles" },
},
};
afterAll(async () => {
await disconnectDB();
});
test("User does not exist in the initial DB", async () => {
const users = await findAllUser();
expect(users).toEqual([]);
});
describe("CRUD user", async () => {
test("create user", async () => {
await createUser(demoUser);
const user = await findUserById(1);
expect(user?.name).toBe("Alice");
expect(user?.email).toBe("alice@prisma.io");
expect(user?.posts[0].title).toBe("Hello World");
expect(user?.posts[0].published).toBe(false);
expect(user?.profile?.bio).toBe("I like turtles");
});
test("update user", async () => {
await updateUserById(1, {
name: "Emma",
email: "emma@prisma.io",
});
const user = await findUserById(1);
expect(user?.name).toBe("Emma");
expect(user?.email).toBe("emma@prisma.io");
});
test("delete user", async () => {
await deleteUserById(1);
const user = await findUserById(1);
expect(user).toBe(null);
});
});
yarn db:test
で実行。 delete user
のテストが失敗する。調べると外部キー制約が原因で失敗している。
修正方針としては色々あるが、 Scheme を見直す方針を取ることにした。
model Post {
- author User @relation(fields: [authorId], references: [id])
+ author User @relation(fields: [authorId], references: [id], onDelete: Cascade, onUpdate: Cascade)
}
model Profile {
- user User @relation(fields: [userId], references: [id])
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
}
更新した Scheme を DB に反映させる。 prisma/migrations/
配下に DB 更新用 SQL が生成されることが確認できる。
$ yarn prisma migrate dev --name update
DB の更新が反映されたので yarn db:test
でテストを実行すると delete user
のテストも成功する。
まとめ
サンプルコードから外れて自力で unit test を書いた経験がほとんどないのでこれで合っているのか不安だがひとまずできた。
Prisma Client 側でテーブルを DROP する API が見当たらなかったのでテスト実行前に prisma migrate reset
を実行するようにしましたが、これより良い方法があったら教えてください。
Discussion