🧪

Vitest で Prisma を使用した DB 接続を行うリポジトリをテストする

2024/03/25に公開
1

目的

この記事では、 Prisma を使用した DB 接続を行うリポジトリに対して、 Vitest でテストします。

Prisma は DB 操作を簡略化する ORM であり、今回のアプリケーションには DB 接続を担当するリポジトリとして実装し、 DB ( PostgreSQL ) に接続してデータを取得・登録・削除できます。
Vitest は、 Jest と互換性のあるテストフレームワークです。今回のアプリケーションでは Remix と Vite を使用しており、 Jest よりもテスト速度が速度が速いと聞くため Vitest を使用しています。

環境

環境 バージョン
Node.js 20.11.1
Yarn 4.1.1
Vite 5.1.0
Prisma CLI 5.11.0
Prisma Client 5.11.0
Vitest 1.4.0

テスト対象

Prisma Client 定義ファイル

// app/db.server.ts
import { PrismaClient } from '@prisma/client';

let prisma: PrismaClient;

declare global {
  // eslint-disable-next-line no-var
  var __db__: PrismaClient | undefined;
}

if (process.env.NODE_ENV === 'production') {
  prisma = new PrismaClient();
} else {
  if (!global.__db__) {
    global.__db__ = new PrismaClient();
  }
  prisma = global.__db__;
  prisma.$connect();
}

export { prisma };

DB 接続を行うリポジトリ

// app/models/post.server.ts
import { Post } from '.prisma/client';
import { prisma } from '~/db.server';

class PostRepository {
  async create(params: Pick<Post, 'title' | 'content'>) {
    const { title, content } = params;
    if (!title || !content) throw new Error('Title and content are required');

    return await prisma.post.create({
      data: {
        title: title,
        content: content,
      },
    });
  }

  async find(params: { id: Post['id'] }) {
    return await prisma.post.findUnique({ where: { id: params.id } });
  }

  async findAll(): Promise<Post[]> {
    return await prisma.post.findMany();
  }

  async delete(params: { id: Post['id'] }) {
    await prisma.post.delete({
      where: { id: params.id },
    });
    return;
  }
}

const postRepository = new PostRepository();

export { postRepository };

テスト用 DB 設定

概要
開発用 DB とは別に、テスト専用 DB を作成します。

テストにおいて開発用 DB を使用すると、既存のデータによってテスト結果が誤判定される可能性があります。テストケースごとに必要なデータのみを準備したテスト専用 DB を利用することで、外部環境に左右されないテスト環境を目指します。

今回のアプリケーションは、 .env を開発環境 DB 接続 URL を定義し、 Prisma が DB に接続します。

作業
ライブラリをインストールします。

yarn add -D dotenv-cli

.env.test を作成します。
.env を開発用とみなし、テスト用として .env.test を用意します。

DATABASE_URL="postgresql://postgres:password@localhost:54320/sample_test"

package.json の scripts を以下の通り設定します。

{
    "scripts": {
        "test": "dotenv -e .env.test -- vitest",
        "db:migrate": "yarn dev:db:migrate && yarn test:db:migrate",
        "dev:db:migrate": "prisma migrate dev",
        "test:db:migrate": "dotenv -e .env.test -- prisma migrate dev --skip-seed",
    }
}

以下のコマンドを実行し、テスト用 DB をマイグレーションします。

yarn db:migrate

コマンドが成功すると、作業完了です。

テスト用モックデータ設定

概要
テストケースによっては事前にデータを登録したいため、モックデータ作成関数を設定します。

DB 接続用 Class のテストでは、データ登録済みの DB が必要なケースがあります。テスト毎に Prisma でデータ登録するコードを定義する方法もありますが、テストケースごとに定義すると重複コードが発生し保守に手間がかかります。
そこで、モックデータ登録のコードを別に設定し一元管理することで、保守性の向上を目指します。

なお、モックデータ作成ライブラリとして fishery を使用します。
Ruby on Rails 経験者向けに説明すると、fishery は factory_bot の開発者達が作成することから、数あるライブラリの中では比較的信頼できると判断し選定しました。

作業
ライブラリをインストールします。

yarn add -D fishery

app/test/factories/post.ts を作成し、以下コードを追記します。
このコードはテスト実装時に使用します。

import { Post } from '@prisma/client';
import { Factory } from 'fishery';

import { postRepository } from '~/models/post.server';

export const postTestFactory = Factory.define<Post>(
  ({ sequence, onCreate }) => {
    onCreate((post) => postRepository.create(post));

    return {
      id: `uuid-${sequence}`,
      title: `test-${sequence}`,
      content: `test-${sequence}`,
      published: true,
    };
  }
);

テスト毎に DB リセット設定

概要
テストケース実行の都度、DB データを初期化します。

現状はテスト実行の都度データ登録されることから、データ取得件数をテストする場合、正常にテストできません。1つのテストが他のテストに影響しないように設定します。

作業
ライブラリをインストールします。

yarn add -D vitest-environment-vprisma

tsconfig.json に以下を追記します。

{
  "compilerOptions": {
    "types": ["vitest/globals", "vitest-environment-vprisma"]
  }
}

vitest.config.ts を以下の通り設定します。

export default mergeConfig(
  baseViteConfig,
  defineConfig({
    test: {
      globals: true,
      environment: 'vprisma',
      setupFiles: ['vitest-environment-vprisma/setup', 'vitest.setup.ts'],
      environmentOptions: {
        vprisma: {
          databaseUrl: process.env.DATABASE_URL,
        },
      },
    },
  })
);

vitest.setup.ts を作成し、以下コードを追記します。
なお、引数の ./app/db.server は、 PrismaClient を export するファイルを指定してください。

import { vi } from 'vitest';

vi.mock('./app/db.server', () => ({
  prisma: vPrisma.client,
}));

ここまでのコードは、次のセクションで使用します。

テスト実装

概要
テスト準備が全て整ったため、テストコードを定義します。

作業
app/models/post.server.test.ts ファイルを作成し、以下のコードを追記します。

import { postTestFactory } from '~/test/factories/post';
import { postRepository } from './post.server';

describe('PostRepository', () => {
  describe('create', () => {
    it('新しいポストを登録できる', async () => {
      // Post 登録に必要なパラメータを生成します
      const params = postTestFactory.build();

      const createdPost = await postRepository.create(params);

      expect(createdPost.title).toBe(params.title);
      expect(createdPost.content).toBe(params.content);
    });
  });

  describe('find', () => {
    it('引数の id が存在すると、ポストを取得できる', async () => {
      // Post のモックデータを DB に登録します
      const post = await postTestFactory.create();
      const foundPost = await postRepository.find({ id: post.id });

      expect(foundPost).toBeDefined();
      expect(foundPost?.id).toBe(post.id);
    });
  });

  describe('findAll', () => {
    it('全てのポストを取得できる', async () => {
      // Post のモックデータを DB に3件登録します
      await postTestFactory.createList(3);

      const posts = await postRepository.findAll();

      expect(posts).toHaveLength(3);
    });
  });

  describe('delete', () => {
    it('引数の id が存在すると、ポストを削除できる', async () => {
      // Post のモックデータを DB に登録します
      const post = await postTestFactory.create();

      await postRepository.delete({ id: post.id });

      const deletedPost = await postRepository.find({ id: post.id });

      expect(deletedPost).toBeNull();
    });
  });
});

yarn test を実行し、以下のようにテストが成功すると作業完了です。

 DEV  v1.4.0 /path/to/remix_fullstack_blog

 ✓ app/_index.test.ts (1)
 ✓ app/models/post.server.test.ts (4)

 Test Files  2 passed (2)
      Tests  5 passed (5)
   Start at  23:33:56
   Duration  326ms (transform 34ms, setup 24ms, collect 25ms, tests 182ms, environment 0ms, prepare 173ms)

 PASS  Waiting for file changes...
       press h to show help, press q to quit

まとめ

実装コードは以下 PR の通りです。
https://github.com/masayuki-0319/remix_fullstack_blog/pull/5

参考 URL

CureApp テックブログ

Discussion