🍰

Prismaで快適にテストを行なうヘルパーを考えた

2021/12/19に公開

はじめに

Prisma: 3.3.0 // 執筆時に使用した Prisma のバージョン
PostgreSQL

データベースを利用したテストは個人的に mock でなく実際に読み書きしたい派です。
となると出てくる問題として、データベースの状態をリセットする方法です。

データベースのお掃除

記事等を探していると以下の方法が見つかりました。

見つけたリセット方法 1

// 参考リンクから抜粋

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();
};

以下のリンクで見つけたのは、PrismaClient の deleteMany で削除する方法です。

https://zenn.dev/5t111111/scraps/f9002ee51a588a

メリット

  • deleteManyを呼ぶだけなので安全な操作

デメリット

  • relation があるときにうまく動作しない
    • Prisma は relation を追加するときRESTRECTで生成するためCASCADEで削除してくれない
    • 枝の先から削除していく必要がある

見つけたリセット方法 2

// 参考リンクから抜粋

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`);
};

1 と同じ方のスクラップにあった別の方法です

https://zenn.dev/5t111111/scraps/f9002ee51a588a

メリット

  • prisma migrate resetを実行するのでDROP TABLECREATE TABLE→seed を行なってくれる
  • 全てのデータを初期状態にできる
  • id を自動採番にしている場合、テーブルの採番が 1 からにリセットされる

デメリット

  • 実行が遅い
    • ファイル毎に実行していたら 1 ファイルあたり 3 秒~かかるのでしんどかった

見つけたリセット方法 3

// 参考リンクから抜粋

const tablenames =
    await prisma.$queryRaw<Array<{ tablename: string }>>`SELECT tablename FROM pg_tables WHERE schemaname='public'`

for (const { tablename } of tablenames) {
  if (tablename !== '_prisma_migrations') {
    try {
      await prisma.$executeRawUnsafe(`TRUNCATE TABLE "public"."${tablename}" CASCADE;`)
    } catch (error) {
      console.log({ error })
    }
  }

公式ドキュメントで全てのデータをリセットするための例として書かれていました。

https://www.prisma.io/docs/concepts/components/prisma-client/crud#deleting-all-data-with-raw-sql--truncate

メリット

  • CASCADEデリートしてくれるため全て削除可能

デメリット

  • id を自動採番にしている場合、テーブルの採番が 1 からにリセットされない

ベストプラクティス

import { PrismaClient } from "@prisma/client";
import { Prisma } from ".prisma/client";

export const resetTable = async (
  modelNames: Prisma.ModelName[]
): Promise<void> => {
  const tablenames = modelNames.map((modelName) => ({ tablename: modelName }));

  for (const { tablename } of tablenames) {
    try {
      await prisma.$executeRawUnsafe(
        `TRUNCATE TABLE "public"."${tablename}" RESTART IDENTITY CASCADE;`
      );
    } catch (error) {
      console.log({ error });
    }
  }
};

テスト向けに以下を反映しました。

  1. 引数で削除したいテーブルを指定可能
    beforeEachなどで使うことで拡張性アップ
  2. RESTART IDENTITYを指定して自動採番をリセット

デメリット

  • seed したデータを消すと復活しない
    • 消したら流し直す必要あり

デメリット解消方法

cleanup の処理をまとめるヘルパーを作成しました。
テストファイルの先頭で関数を実行することでbeforeAll, afterAllでいい感じにデータベースのリセットと seed の代わりとなる処理を行なえます。

// seedの対象
// - adminにTEST_ADMINS
// - userにTEST_USERS

export const cleanup = (modelNames: Prisma.ModelName[] = []) => {
  beforeAll(async () => {
    // seed対象のテーブルは常にリセット
    await resetTable(["Admin", "User", ...modelNames]);
    // 以下でseed
    await prisma.admin.createMany({ data: TEST_ADMINS });
    await prisma.user.createMany({ data: TEST_USERS });
  });
  afterAll(async () => {
    await resetTable(["Admin", "User", ...modelNames]);
    await prisma.$disconnect();
  });
};

おわりに

Prisma便利だァ...

Discussion