💨

DrizzleでHonoのDBテスト用のフィクスチャ作成

2025/01/13に公開

注意

ただ作成といっても以下のものをDrizzle用に移植したものになります。

https://engineering.mercari.com/blog/entry/20220411-42fc0ba69c/

はじめに

バックエンドのテストを書く際、あらかじめデータベースの状態をフィクスチャとして準備し、テストを行うというのをよくやります。
RailsDjangoなどのフレームワークには必ずと言って良いほど入っており、非常に助かります。

ですがHonoでのDBフィクスチャのテストの方法を探しても(観測した範囲では)見つからず、だったら作ってしまおうかとなり作成しました。
備忘録として記事にしています。

コードは以下のリポジトリにおいています。

https://github.com/maguro-alternative/hono-next-test-sample

前提

利用するものは以下の通りです。

  • ライブラリ&フレームワーク
    • Hono
    • Drizzle
    • Next.js
    • jest
    • postgres
  • データベース
    • PostgreSQL

Postgresで以下のテーブルを利用したTodoアプリを例として話します。

drizzleでのスキーマ定義
schema.ts
import { integer, serial } from 'drizzle-orm/pg-core';
import {
  timestamp,
  pgTable,
  text,
  boolean,
} from 'drizzle-orm/pg-core';

export const todos = pgTable('todos', {
  id: serial().primaryKey().notNull(),
  name: text().notNull(),
  done: boolean().notNull(),
  created_at: timestamp(),
  updated_at: timestamp(),
});

export const assignees = pgTable('assignees', {
  todoId: integer().primaryKey().references(() => todos.id),
  name: text().notNull(),
  created_at: timestamp(),
  updated_at: timestamp(),
});
テーブル
postgres=# \d+ public.todos
                                                            テーブル"public.todos"
     列     |           タイプ            | 照合順序 | Null 値を許容 |            デフォルト             | ストレージ | 圧縮 | 統計目標 | 説明 
------------+-----------------------------+----------+---------------+-----------------------------------+------------+------+----------+------
 id         | integer                     |          | not null      | nextval('todos_id_seq'::regclass) | plain      |      |          | 
 name       | text                        |          | not null      |                                   | extended   |      |          | 
 done       | boolean                     |          | not null      |                                   | plain      |      |          | 
 created_at | timestamp without time zone |          |               |                                   | plain      |      |          | 
 updated_at | timestamp without time zone |          |               |                                   | plain      |      |          | 
インデックス:
    "todos_pkey" PRIMARY KEY, btree (id)
参照元:
    TABLE "assignees" CONSTRAINT "assignees_todoId_todos_id_fk" FOREIGN KEY ("todoId") REFERENCES todos(id)
アクセスメソッド: heap

postgres=# \d+ public.assignees
                                               テーブル"public.assignees"
     列     |           タイプ            | 照合順序 | Null 値を許容 | デフォルト | ストレージ | 圧縮 | 統計目標 | 説明 
------------+-----------------------------+----------+---------------+------------+------------+------+----------+------
 todoId     | integer                     |          | not null      |            | plain      |      |          | 
 name       | text                        |          | not null      |            | extended   |      |          | 
 created_at | timestamp without time zone |          |               |            | plain      |      |          | 
 updated_at | timestamp without time zone |          |               |            | plain      |      |          | 
インデックス:
    "assignees_pkey" PRIMARY KEY, btree ("todoId")
外部キー制約:
    "assignees_todoId_todos_id_fk" FOREIGN KEY ("todoId") REFERENCES todos(id)
アクセスメソッド: heap

postgres=#

MySQLでも問題なく動くはずです。

フィクスチャとは?

CakePHPのドキュメントがわかりやすい表現をしていたので引用します。

https://book.cakephp.org/4/ja/development/testing.html#:~:text=なりません。-,フィクスチャー,テーブルを空にします。

テストコードの挙動がデータベースやモデルに依存するとき、テストに使うためのテーブルを生成し、 一時的なデータをロードするために フィクスチャー を使うことができます。 フィクスチャーを使うことにより、 実際のアプリケーションに使われているデータを破壊することなく テストができるというメリットがあります。 また、アプリケーションのためのコンテンツを実際に用意するより 先にコードをテストすることができます。

これによりデータがある場合のAPIレスポンスを確認することや、データがない場合の挙動をテストで再現することができます。

フィクスチャの流れ

fixtures.tsとそれぞれのテーブルに対応するassignee.tstodo.tsを作成します。

.
├── fixture
│   ├── assignee.ts
│   ├── fixtures.ts
│   └── todo.ts
assignee.ts
todo.ts

例としてtodoモデルの流れを見ていきます。

  • テーブルに対応するモデルを作成。

https://github.com/maguro-alternative/hono-next-test-sample/blob/56f07300400e4e039629523219ca422308cb455b/fixtures/todo.ts#L4-L10

  • フィクスチャ作成の関数を作成。

https://github.com/maguro-alternative/hono-next-test-sample/blob/56f07300400e4e039629523219ca422308cb455b/fixtures/todo.ts#L12-L14

  • デフォルト値を宣言。

https://github.com/maguro-alternative/hono-next-test-sample/blob/56f07300400e4e039629523219ca422308cb455b/fixtures/todo.ts#L16-L22

  • 後述のModelConnectorクラスに起こす。

https://github.com/maguro-alternative/hono-next-test-sample/blob/56f07300400e4e039629523219ca422308cb455b/fixtures/todo.ts#L24-L51

fixtures.ts

https://github.com/maguro-alternative/hono-next-test-sample/blob/56f07300400e4e039629523219ca422308cb455b/fixtures/fixtures.ts

ModelConnectorクラスでモデルの管理や、データベースに挿入を行います。

以下のように、テーブルに対応するモデルをフィクスチャに定義します。

https://github.com/maguro-alternative/hono-next-test-sample/blob/56f07300400e4e039629523219ca422308cb455b/fixtures/fixtures.ts#L7-L12

それぞれの引数は以下のような役割を持っています。

model

  • モデルオブジェクト。テーブルに対応するモデルを入れます。
    • 上記の場合、todoのモデルが入ります。

https://github.com/maguro-alternative/hono-next-test-sample/blob/56f07300400e4e039629523219ca422308cb455b/fixtures/todo.ts#L16-L22

setter

  • モデルの初期値を設定します。
    • 引数としてmodelオブジェクトを持ってきます。
    • ここのオブジェクトを書き換えることで、用意したいデータを挿入することができます。

https://github.com/maguro-alternative/hono-next-test-sample/blob/56f07300400e4e039629523219ca422308cb455b/app/api/[[...route]]/todo.test.ts#L19-L22

  • また、メソッドチェーンを使って複数のモデルやデータの書き込みができます。
fixture = await fixtureBuild(fixture,
  newTodo(
    (todo) => {
      todo.name = 'todo';
      todo.done = false;
    }
  ),
  newTodo(
    (todo) => {
      todo.name = 'todo2';
      todo.done = false;
    }
  )
);

addToFixture

  • setterで設定したモデルをフィクスチャに追加します。

https://github.com/maguro-alternative/hono-next-test-sample/blob/56f07300400e4e039629523219ca422308cb455b/fixtures/todo.ts#L31-L33

connect

  • 後述のconnectWithで他のモデルと接続する際に結合するモデルを入れます。

insertTable

  • モデルをデータベースに挿入します。

https://github.com/maguro-alternative/hono-next-test-sample/blob/56f07300400e4e039629523219ca422308cb455b/fixtures/todo.ts#L39-L50

connectWith

  • 結合するモデルの初期値を設定します。
    • assigneestodo_idtodosの主キーにあたり、assignees単体で挿入ができません。
      • connectを使うことでモデルの関連を表現しやすくなり、外部キーを勝手に挿入するようになります。
      newTodo(
        (todo) => {
          todo.name = 'todo';
          todo.done = false;
        }
      ).connectWith(
        newAssignee(
          (assignee) => {
            assignee.name = 'user';
          }
        )
      )
      

addToFixtureAndConnect

  • 上記のconnectWithのモデルをフィクスチャに追加し、データベースに挿入を行います。

以上をfixtureBuildにまとめて、データベースに挿入したデータをモデルとして呼び出せるようにします。

フィクスチャの使い方

todo一覧を取得し、jsonを返すAPIがあります。

https://github.com/maguro-alternative/hono-next-test-sample/blob/56f07300400e4e039629523219ca422308cb455b/app/api/[[...route]]/todo.ts

このtodoの取得のテストをしてみます。

https://github.com/maguro-alternative/hono-next-test-sample/blob/56f07300400e4e039629523219ca422308cb455b/app/api/[[...route]]/todo.test.ts

  • トランザクションを張り、フィクスチャを作成します。

https://github.com/maguro-alternative/hono-next-test-sample/blob/56f07300400e4e039629523219ca422308cb455b/app/api/[[...route]]/todo.test.ts#L15-L16

  • フィクスチャにモデルを追加し、テーブルに挿入します。

https://github.com/maguro-alternative/hono-next-test-sample/blob/56f07300400e4e039629523219ca422308cb455b/app/api/[[...route]]/todo.test.ts#L17-L24

  • requestメソッドでGetリクエストを呼び出します。

https://github.com/maguro-alternative/hono-next-test-sample/blob/56f07300400e4e039629523219ca422308cb455b/app/api/[[...route]]/todo.test.ts#L25-L27

  • 200のステータスコードと期待したレスポンスが帰ってくるか確認します。

https://github.com/maguro-alternative/hono-next-test-sample/blob/56f07300400e4e039629523219ca422308cb455b/app/api/[[...route]]/todo.test.ts#L28-L39

  • ちゃんとテストがpassするはずです。

テストが終わり次第、トランザクションをロールバックさせます。
しかしDrizzleはロールバックをするとエラーを起こすらしく、以下のような例外処理を毎回挟む必要があります。

https://github.com/maguro-alternative/hono-next-test-sample/blob/56f07300400e4e039629523219ca422308cb455b/app/api/[[...route]]/todo.test.ts#L42-L47

まとめ

いかがでしたでしょうか?
フィクスチャは実DBを利用してテストを行うので、外部キー制約などに気づきやすくなります。

TypeScript初心者なので間違いや修正点などがあれば指摘していただけると幸いです。

GitHubで編集を提案

Discussion