Prisma使っててテストをやるときどうすればいいの
Prisma を使っていて、テストをどうするか考えている。
Prisma でテストをやるときの方法について全然情報がない。
実際にデータベースを使ったテストがしたい
PrismaClient
をがんばってモックしてデータベースアクセスが発生しないようにすることもできなくはないが、Prisma がまだ若いライブラリであることもあり、ユニットテストの時点でもライブラリの実際の挙動をテストできた方が嬉しいと思っている。
テストのときに使うデータベースはテスト用のものにしたい
例えば、数少ない情報の中でこの記事では、Docker で Node と MySQL コンテナを動かしてそこでテストをやる手順が書いてある。
テスト用のデータベースは開発用のものとはわけたい。当然です。でも、Prisma ではテスト用に接続先の DB を変えるということがやりにくい (要は DATABASE_URL
を切り替えるのがだるい) 。なので上記のポストはそれに対応するためのものである。
これはその目的のためにはうまくいきそう。とはいえ、環境変数の切り替えのためだけにやることとしてはちょっと面倒すぎるという感じがしている。
普通にシェルから jest を実行するときに
DATABASE_URL="mysql://root:root@localhost:33306/foobar" npm test
とするのでいいんじゃないかという気がしていて、実際そうしようと思う。
Nest.js で Prisma を使っているときにテストの実行後に接続が切れない
Nest.js で Prisma を使うときは、まずはおそらく公式ドキュメントにあるこの方法で使うパターンがほとんどだと思う。
でもこれでやると、DB アクセスが発生するテストを jest で実行したときに接続が切れなかった。待ち状態になって jest がタイムアウトする。
Nest.js のライフサイクルでプロバイダーの onModuleDestroy
は SIGTERM
とかを受けたときに実行されるシグナルハンドラのようなフックのようだ。
よくわかってないけど、まあ直接 onModuleDestroy
を呼べば $disconnect()
されるだろうと思って雑に↓のような感じで解決した。実際問題は解決した。
afterAll(async () => {
await (service as any).prismaService.onModuleDestroy();
});
これは一旦これでよし
テストを実行する前にマイグレーションを実行したい
テストを実行するときに必ずマイグレーションが走るようにしたい。
マイグレーションは test suites 全体を実行する前に1度実行すれば十分なので、以下のようにテストを実行することで対応することにした。
npx prisma migrate reset --force && npm test src/modules/albums
実際には ここに書いているように 、接続先 DB の切り替えのために各コマンドの前に環境変数を設定するようにして実行する。
prisma migrate
を実行すると Prisma クライアントも自動生成される。
テストを実行する前にデータベースの掃除がしたい
テストを実行するときは、データベースに前のテストのデータが残っていたりとかがない綺麗な状態になっていて欲しいので、データベースを掃除するようにしたい。
遅くはなるだろうが、これはすべてのテストの前に実行すべき。
ここにやり方が書いてあったが、全部動かなかった :cry:
ここで書かれているアプローチは2つある。
-
Reflect.ownKeys(Object.getPrototypeOf(prisma))
で PrismaClient のリフレクションからモデル名を取得する方法- → PrismaClient のプロトタイプのキーにモデル名はない (仕様変更なのかトランスパイルの結果が異なるのかなどは調べてない)
-
dmmf
(data model meta format の略らしい) からモデル情報を取得する方法- →
import { dmmf } from '@prisma/client';
がいきなりできない。export
されてない
- →
リフレクションに頼るよりも後者の dmmf を使う方法の方が良さそうだが、いずれにせよどっちも動かないのでどうしようもない。
とりあえず @prisma/client
の型定義を眺めながら、どうやら以下のいずれかで修正すれば動くことがわかった。
import { PrismaClient, Prisma } from '@prisma/client';
export const cleanupDatabase = () => {
const prisma = new PrismaClient();
const modelNames = Prisma.dmmf.datamodel.models.map((model) => model.name);
return Promise.all(
modelNames.map((modelName) => prisma[modelName.toLowerCase()].deleteMany())
);
};
or
import { PrismaClient } from '@prisma/client';
export const cleanupDatabase = () => {
const prisma = new PrismaClient();
const propertyNames = Object.getOwnPropertyNames(prisma);
const modelNames = propertyNames.filter(
(propertyName) => !propertyName.startsWith('_')
);
return Promise.all(modelNames.map((model) => prisma[model].deleteMany()));
};
適当な修正だが一応コメントしておいた。
jest から実行すると、さっきのタイムアウト問題が発生したので、最終的には↓を使うことにしてみた。
import { PrismaClient, Prisma } from '@prisma/client';
/**
* 全てのテーブルのデータを削除する
* 参考: https://github.com/prisma/docs/issues/451
*/
export const cleanupDatabase = async (): Promise<void> => {
const prisma = new PrismaClient();
const modelNames = Prisma.dmmf.datamodel.models.map((model) => model.name);
await Promise.all(
modelNames.map((modelName) => prisma[modelName.toLowerCase()].deleteMany())
);
prisma.$disconnect();
};
テストケース側では普通にこう
beforeEach(async () => {
await cleanupDatabase();
// ... いろいろ
});
このスクラップ と、その中で参照されている https://github.com/ctrlplusb/prisma-pg-jest では、Prisma クライアントではなく pg
/ mysql
のクライアントを直接使ってデータベースの操作をしているようだ。
データベースの掃除には、ここで書いたように個別テーブルを削除していくのではなく、 migrate reset
を使った方が効率的だしシンプルに解決できそうということがわかった。
したがって、以下のような処理をテスト実行前に流す。
import util from 'util';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const exec = util.promisify(require('child_process').exec);
export const resetDatabase = async (): Promise<void> => {
const prismaBinary = './node_modules/.bin/prisma';
await exec(`${prismaBinary} migrate reset --force`);
};
ちなみに MySQL の場合だけ確認したが、migrate reset
の実装はごくシンプルで、DROP DATABASE
/ CREATE DATABASE
をしているだけの様子。
↑のコメントで書いていた方法だと、例えば VSCode や WebStorm などからテストを実行する、つまりシェルを経由しないテスト実行の際に、migrate reset
が内部的に実行している seeding で ts-node
が見つからないエラーが出た。
以下に変更することで回避している。
import util from 'util';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const exec = util.promisify(require('child_process').exec);
export const resetDatabase = async (): Promise<void> => {
await exec('npx migrate reset --force');
};
今になっても結構このスクラップに LIKE をつけてくれる人がいるので (ありがとうございます)、これを書いてから2年くらい経った今どういうやり方でやっているか簡単に書いておきたいと思います。
基本的な構成と考え方
考え方はあんまり変わっていません。
- データベースはテスト用のデータベースを1つだけ用意している
- 個別のテストごとにデータベースを用意することはしていない
-
docker compose
でテスト用と開発用が立ち上がるようにしている (大抵の場合は Dev container と一緒に使う)
- ユニットテストでも Prisma Client の API 操作のスタブはできるだけしない
- 後述のユニットテスト用のツール (vitest-environment-vprisma) によってモック化はしているが、実際に実行される API は Prisma Client のそれであり、実際のデータベースを操作するようにしている
- このスクラップを書いたときは Nest.js で Prisma を使う前提だったが、今はあまり Nest.js を使っていないので
PrismaService
などを前提にした構成になっていない (インスタンス化した PrismaClient インスタンスをシングルトンでアプリケーションのコンテキストに生やす)
ユニットテスト
- テスト用のフレームワークには Vitest を使っている
- ユニットテスト用の環境変数セット (
.env.test
) を用意し、それを読み込むようにしている- 実行例:
dotenv -e .env.test vitest run
- 実行例:
-
vitest-environment-vprisma を使って、個別のテストケースがトランザクション実行/ロールバックされるようにしており、それをテスト前後のデータベースの掃除としている
- アプリケーションはシングルトンの Prisma Client インスタンスを使うように構成して、このツールでモック化する
- テストデータの準備にはそれ用のツールなどは使っていない
- 普通に PrismaClient の upsert を使ってデータを作成している
- faker くらいは使うが、factory_bot 的な仕組みを導入していないということ
- テストを実行する前に1度
prisma db push
を実行するようにしている-
prisma migrate reset
ではない - これは単純に PlanetScale を使うことがあるのでマイグレーションが使いにくいため (本当は使いたい)
-
E2E テスト
- Playwright を使っている
- E2E テストは基本的に開発環境に対して実行されるようにしている
- つまり、
.env.test
は E2E では使われない
- つまり、
- E2E テストでのテストデータの掃除などは仕組み化していない。汚れても DB をリセットして seed を読み込むので済むことが多いので