なぜ pgmem か
PostgreSQL に触るテストは、どこかから PostgreSQL を調達しなければなりません。どのチームもこの問いに一度は答えを出しています。コンテナを起動する、マシンに入れたサーバーを使い回す、データベースをモックに置き換えてその外側だけをテストする。どの答えにも代償があります。1〜2 秒の起動時間だったり、すべてのマシンにあるとは限らないデーモンへの依存だったり、SQL が本当に実行されたという確信だったりします。このページでは pgmem をそうした定番の答えと並べて、起動時間、安定性、並列性、何を本当にテストしているか、どの言語から使えるか、どの拡張機能が動くか、の 6 つの軸で比べます。
pgmem、Testcontainers、docker run、devbox の時間はベンチマークのページと同じで、bench/alternatives と bench/modelcase のハーネスで計ったものです。PGlite と embedded-postgres はハーネスに含めておらず、同じマシンで手計測した値をグラフにそう注記して載せています。モックには計るものがありません。
比較する選択肢
Section titled “比較する選択肢”- pgmem:PostgreSQL 18.3 を WebAssembly にコンパイルして Go に変換し、テストプロセスの中(Python、Java、Node.js では同梱のバイナリ)でメモリ上のファイルシステムを使って本物のクラスタとして動かします。スナップショットと copy-on-write のフォークを持ちます。
- PGlite:pgmem の PostgreSQL ビルドの元になっているプロジェクトです。JavaScript 向けに WebAssembly にコンパイルした PostgreSQL で、Node、Bun、Deno、ブラウザの中でシングルユーザーモードで動きます。
- Testcontainers:公式の
postgresイメージを Docker API 経由で起動し、reaper コンテナで後始末するライブラリです。 docker run:同じイメージをスクリプトや Makefile から起動するやり方です。- devbox:nix でネイティブにインストールした PostgreSQL をサービスとして動かします。データディレクトリはディスク上です。
- embedded-postgres:ネイティブの PostgreSQL バイナリをダウンロードし、
initdbを実行して postmaster を起動するライブラリ群です。Java の zonky、Go の fergusstrange、Node.js の embedded-postgres があります。 - 自作モック:自分のリポジトリインターフェースをメモリ上で実装したもので、テスト対象のコードはデータベースを一切見ません。
- go-sqlmock:
database/sqlのドライバとして振る舞い、SQL の文字列が期待値に一致したら台本どおりの行を返します。pgx 向けには pgxmock があります。
| 起動から最初のクエリまで | 壊れる条件 | 更新テスト | 動くもの | 対応言語 | 拡張機能 | |
|---|---|---|---|---|---|---|
| pgmem | 70 ms | インストール不要 | フォークで並列 | PostgreSQL 18.3 | Go · Python · Java · Node.js。ほかはバイナリ経由 | 33 個組み込み、pgvector 含む |
| PGlite | インスタンスごと 0.7 s | インストール不要 | インスタンスにつき接続 1 本 | PostgreSQL 18.3、シングルユーザー | JavaScript · TypeScript。wasm ランタイムがあれば直接 | 生成時にパッケージを渡す |
| Testcontainers | 1.1 s + イメージ取得 | Docker デーモンがない | テンプレート複製、直列 | 任意のバージョン | Java · Go · Python · Node.js · .NET · Rust … | contrib。ほかは別イメージ |
| docker run | 1.6 s + イメージ取得 | Docker がない。後始末は自前 | テンプレート複製、直列 | 任意のバージョン | シェルから何でも | contrib。ほかは別イメージ |
| devbox | 0.4 s、クラスタ準備済み | nix がない。状態がディスクに残る | テンプレート複製、直列 | 任意のバージョン | シェルから何でも | contrib。ほかは nix パッケージ |
| embedded-postgres | 約 5.1 s | ネイティブバイナリをディスクに展開 | テンプレート複製、直列 | 同梱バージョン | Java · Go · Node.js | contrib のみ |
| 自作モック | なし | 壊れない。黙って乖離 | 自明に並列 | 自分のモック | 何でも | — |
| go-sqlmock | なし | SQL の文字列が変わる | 自明に並列 | SQL 文字列の一致 | Go | — |
起動時間は中央値です。出典は「起動時間」の節に書いてあります。
起動から最初のクエリまで
- pgmem70 ms
- PGlite(最初のインスタンス)712 ms
- PGlite(2 つ目以降)483 ms
- devbox411 ms
- Testcontainers1.09 s
- docker run1.65 s
- embedded-postgres5.12 s
- 自作モック / go-sqlmockなし
新しいプロセスを 1 つ起動し、SELECT 1 が通るまで。pgmem、Testcontainers、docker run、devbox はベンチマークハーネスの中央値(2026-09-18 と 2026-09-14)です。PGlite 0.5.8 と embedded-postgres は同じマシンで 2026-09-20 に手で計った中央値(それぞれ 6 回と 5 回)です。モックには起動するサーバーがありません。
70 本のテストスイートを通しで
- pgmem204 ms
- devbox646 ms
- Testcontainers1.73 s
- docker run2.48 s
参照 50 本、更新 20 本のモデルケースを、サーバー起動から最後のテストまで通した 5 回の中央値(2026-09-19)。共有サーバーでは更新テストが順番を待ち、pgmem ではフォークの上で横に並んで走ります。PGlite と embedded-postgres はハーネスに含めていません。
共有サーバーの起動はテスト実行 1 回につき 1 度なので、1.5 秒なら安いように聞こえます。ところが、テストがいちばん頻繁に走る場面ではそうなりません。エディタから 1 本だけ走らせるとき、watch モード、パッケージ単体のテスト、12 ジョブの CI マトリクス。そのどれもがサーバーをもう一度起動します。
pgmem が 70 ms ほどで起動するのは、initdb を実行しないからです。データディレクトリはビルド時に initdb した結果を圧縮した tar として埋め込んであり、サーバーの起動はそれをメモリに展開して postmaster を立ち上げるだけです。devbox の 0.4 s がいちばん近いものの、それは計測前にクラスタを初期化してあるからです。コンテナは取得済みのイメージでも 1.1〜1.6 s かかり、マシンで初めて走るときには 118 MB の取得が加わります。サーバー方式で最も遅いのが約 5 s の embedded-postgres で、Go 版は起動のたびに 33 MB のアーカイブを 240 MB のバイナリに展開して initdb を実行します。初回はアーカイブのダウンロードも加わります。
PGlite はその中間です。プロセス内の最初のインスタンスが 0.7 s、2 つ目以降が 0.5 s でした。この値はそのまま分離のコストでもあります。PGlite にフォークはないので、きれいなデータベースが要るテストは別のインスタンスを起動します。
モックの起動時間はゼロで、この軸でのモックの利点はそれがすべてです。
データベースのフィクスチャが安定しているかどうかは、2 つの問いで決まります。テストが走るすべてのマシンで起動するか。テストが途中で死んだとき、何かを残さないか。
Testcontainers と docker run には Docker デーモンが要ります。 開発者の Linux マシンや GitHub がホストする Linux ランナーにはあります。macOS ランナー、Windows ランナー、管理の厳しい社用ラップトップ、そして別のコンテナの中には、ないことのほうが多く、そのときは 2 つ目のテスト実行方法を用意することになります。Testcontainers は reaper で確実に後始末しますが、スクリプトの docker run はジョブがキャンセルされるとコンテナを残し、固定のホストポートは同じランナーの次のジョブと衝突します。
devbox と embedded-postgres はネイティブバイナリを動かします。 devbox はマシンに nix が必要で、クラスタをディスクに持つので、スクリプトが消さない限り状態は実行をまたいで残ります。embedded-postgres は Maven Central からプラットフォームごとのバイナリを取得してディスクに書きます。Go 版が起動した postmaster は、SIGKILL で止められたテストプロセスより長生きします。
PGlite と pgmem はデータベースサービスのインストールが要りません。 どちらも言語ランタイムが動くところで動くパッケージです。pgmem はデータベースのファイルをメモリに保持し、既定ではループバック TCP で待ち受けます。Unix socket も選べ、その場合は専用の socket ディレクトリを作ります。Python、Java、Node.js と、Go で任意に選べる外部バイナリは、親が stdin を閉じると終了します。Go から外部プロセスを使う場合は、バイナリを別途インストールします。
pgmem にも制限はありますが、設計時にぶつかる種類のもので、ランダムに起きるものではありません。PostgreSQL 18.3 であり、そのバージョンだけです。TLS もパスワード認証も ICU 照合順序もありません。独自のバックグラウンドワーカーを必要とする拡張は同梱していません。サーバーは 32 ビットの WebAssembly で、アドレスできるのは最大 4 GiB です。制限事項のページに全部載せています。
モックは最も安定したフィクスチャで、最も情報の少ないフィクスチャでもあります。 モックは起動に失敗しません。同時に、列の名前が変わったことも、ユニーク制約が増えたことも、書かれた当時のクエリがもう存在しないことも、決して気づきません。変化するものをテストするのをやめたから安定しているのです。
選択肢の差がいちばん大きいのがここで、それはチューニングの問題ではなく構造の問題です。
コンテナ、devbox、embedded-postgres のどれであれ、共有サーバーは本物のマルチプロセス PostgreSQL です。参照テストはその上で並列に走れます。更新テストはそうはいきません。注文を 1 件挿入して行数を数えるテストが 2 本あれば互いに見えてしまうので、1 台のサーバー上のスイートは更新テストを TRUNCATE と再シードの後ろに 1 本ずつ並べるか、テンプレートから複製したデータベースを更新テストごとに渡すことになります。後者はテストあたり 20〜80 ms かかるうえ、複製中はテンプレートに誰も接続していてはいけません。
pgmem はマイグレーションとシードの後にスナップショットを 1 回取り、更新テストはそれぞれフォークします。フォークは、データディレクトリの copy-on-write コピーの上で自分のポートと自分のバッファキャッシュを持つ完全なクラスタで、1 つだけなら約 6 ms、8 つ同時でも約 10 ms です。フォーク同士は独立なので更新テストは横に並んで走り、同時に生きる数は MaxForks で抑えます。既定値はプロセスが使えるメモリから決まります。上のグラフの 70 本のスイートは pgmem で約 200 ms、Testcontainers で 1.7 s かかり、差の大半は更新テストが列に並ぶか横に並ぶかです。
PGlite は PostgreSQL をシングルユーザーモードで動かします。インスタンス 1 つが接続 1 本で、文は 1 つずつ実行されます。接続を 1 本しか使わないテストには十分です。ただ、セッションが 2 つ必要なものは何も試せません。ロック待ち、デッドロック、SELECT … FOR UPDATE SKIP LOCKED、アドバイザリーロック、開いているトランザクションの外でクエリを投げるアプリケーション。並列テストは並列インスタンスを意味し、1 つにつき 0.5 s と WebAssembly のヒープがかかります。
モックは自明に並列で、並行性について何も証明しません。中で何も並行していないからです。
何を本当にテストしているか
Section titled “何を本当にテストしているか”データベースのフィクスチャは、ひとつの問いに答えるためにあります。このコードは PostgreSQL に対して動くのか。選択肢によって、この問いへの答え方はまるで違います。
pgmem、PGlite、そして本物のサーバーはすべて、PostgreSQL 自身のパーサ、プランナ、エグゼキュータを動かします。間違った結合、制約違反、何も返さない RETURNING、2 回発火するトリガー。どれも本番で失敗するのと同じ形で、PostgreSQL の SQLSTATE を伴ってテストで失敗します。pgmem とサーバーはマルチセッションの振る舞いも再現します。ロックは待ち、デッドロックは検出され、pg_stat_activity にはほかのセッションが見えます。
go-sqlmock がテストするのはもっと狭いことです。コードがテストの期待どおりの SQL 文字列を、期待どおりの順序で送ったこと、そして台本どおりの行を処理できたこと。SQL は実行されません。列名にタイポのあるクエリは、期待値に同じタイポがあれば通ります。速くするために書き直したクエリは、正規表現をすべて直すまで落ち続けます。ORM を使っていれば、期待値は ORM の出力から逆算した文字列で、ORM を上げると変わります。
自作モックはリポジトリより上の層をテストし、下の層は何もテストしません。リポジトリ、クエリ、スキーマをカバーするのは、別にある統合テストだけです。そしてそれはしばしば存在しません。統合テストを避けるためにモックを採用したのですから。制約とトランザクションは手で模倣され、そのモデルは PostgreSQL のものより単純で、黙って乖離していきます。
一方で、モックには本物のサーバーが頼まれてもできないことがあります。選んだとおりに失敗することです。メモリ不足のエラー、too_many_connections での接続拒否、トランザクションの途中で切れる接続、30 秒固まるクエリ。コードがこれらをどう扱うかを見たいテストは、モックなら 1 行で書けますが、本物の PostgreSQL では手間と運が要ります。BeOS にはまさにこの手の状況のために is_computer_on_fire() という API がありました。それが true を返す場所がモックです。
データベースに触る筋合いのないロジックと、健全なサーバーが起こしてくれないエラー経路には、モックが正しい道具です。SQL を中心に組み立てられたコードをテストするとき、モックはデータベースの代わりになりません。
pgmem は Go、Python、Java、Node.js 向けに提供しています。Go パッケージはテストプロセスの中でクラスタを動かし、ほかの 3 つは同じコードからビルドした単独バイナリを同梱して stdin 経由で操作するので、フォークの振る舞いはどの言語でも同じです。公式パッケージのない言語も締め出されてはいません。バイナリは通常の PostgreSQL ワイヤプロトコルを話すのでどのドライバからでも接続でき、制御は stdin の JSON 行です。別の言語への対応は薄いラッパーを書くだけで、移植ではありません。
PGlite はまず JavaScript のライブラリです。Node、Bun、Deno、ブラウザで動きます。エンジンは WebAssembly モジュールなので、WebAssembly ランタイムのある言語からなら、モジュールのインポートをその言語で束ねて直接動かすこともできます。
Testcontainers には Java、Go、Python、Node.js、.NET、Rust、Ruby などの公式ライブラリがあり、docker run や devbox はフィクスチャがテストプロセスの外にあるので、どの言語からでもスクリプトで扱えます。
embedded-postgres は 1 つのプロジェクトではなく、独立した移植の集まりです。Java、Go、Node.js にそれぞれメンテナンスされているものがあり、起動の最適化もバイナリの取得元も違います。Python には広く使われている同等品がなく、pytest-postgresql はマシンにインストール済みの PostgreSQL を前提にします。
自作モックはどの言語でも書けます。go-sqlmock は Go 専用で、ほかの言語の親戚(JDBC 接続に対する Mockito、カーソルに対する unittest.mock)も同じ性質を持ちます。テストしているのは文字列であって、SQL ではありません。
本番に届くころには、たいていのスキーマは拡張機能を少なくとも 1 つ使っています。キーには pgcrypto や uuid-ossp、検索には pg_trgm と citext、データモデルには hstore や ltree、そして最近は vector。マイグレーションが名指しするものがテスト用データベースにないと、テストが 1 本走る前に最初の CREATE EXTENSION で止まります。
pgmem は 33 個の拡張機能をビルドに組み込んであります。マネージドサービスと PGlite が提供しているものから、ORM が使うものを優先して選びました。contrib の型とインデックス、pg_trgm、pgcrypto、uuid-ossp、pg_stat_statements、pgvector など、全部の一覧は拡張機能のページにあります。どれも何もインストールせずに CREATE EXTENSION が通るので、本番で流したマイグレーションがそのままテストでも流れます。入れていないものは意図的です。pg_cron のような独自のバックグラウンドワーカーを持つ拡張、dblink や postgres_fdw のようなネットワークに出る拡張、ほかの言語ランタイム、そして PostGIS。足りない contrib モジュールを追加する手順は文書化してあり、ドライバスクリプトとエージェント用スキルがあって、リビルド 1 回で済みます。
PGlite は拡張機能を別パッケージとして配り、インスタンス生成時に渡します。一覧は pgvector を含めてほぼ同等です。Docker イメージには contrib モジュールしか入っておらず、pgvector や PostGIS はそれぞれのイメージに切り替えるか Dockerfile を書くことになります。Testcontainers も docker run もそれを受け付けます。devbox は nix パッケージの contrib を持ち、ほかは設定に nix パッケージを足して加えます。embedded-postgres は contrib だけを同梱していて、それ以上は自分でバイナリのアーカイブを作ることになります。モックには語るべき拡張機能がありません。SQL を実行しないからです。
ほかを選ぶべきとき
Section titled “ほかを選ぶべきとき”pgmem が PostgreSQL のテストすべてへの答えというわけではありません。
- 本番が別のメジャーバージョンで動いていて、そのバージョンのプランナや機能が必要なとき。Testcontainers、
docker run、devbox、embedded-postgres は指定したバージョンを動かします。 - TLS、パスワード認証、ICU 照合順序、
pg_cronのようなバックグラウンドワーカー付きの拡張が必要なとき。 サーバーにはあり、pgmem にはありません。 - データが 4 GiB を超えるとき、あるいは 1 本のテストが 32 ビットのアドレス空間に収まらないとき。ネイティブのサーバーを使ってください。
- コードがブラウザで動き、隣にデータベースが必要なとき。それが PGlite の目的で、このページのほかの選択肢にはできません。
- テスト対象が純粋なロジックで、たまたまリポジトリインターフェースを受け取っているだけのとき。モックのほうが書くのも走らせるのも速く、リポジトリは pgmem で別にテストできます。
- サーバーが合図どおりに起こしてくれない失敗をテストしたいとき。 メモリ不足、接続拒否、トランザクション途中の切断など。モックや障害注入するドライバなら決定的に再現できます。
Go、Python、Java、Node.js のサービス層やデータ層を CI とエディタでテストする、というごく普通の場合には、pgmem はライブラリ 1 つの import と引き換えに本物のデータベースを渡し、共有サーバーには出せない並列性を渡します。