Parallel test with PostgreSQL in Clojure

一般的にテストコードが増えれば増えるほど、テストの実行時間は伸びていきます。この伸びてしまったテストの実行時間を短かくするための方法は幾つかあります。代表的なものではインメモリ DB を利用する、いつも実行するテストと任意のタイミングのときだけ流すテストを分ける、並列にテストを実行するなどがあります。今日は並列でテストを実行する話にフォーカスしていくことにします。

Clojure でテストを並列実行したい場合、 eftest を利用すれば簡単に実現することができます。しかしながら、データベースを利用している場合、単に eftest を使っただけでは問題が起こります。場合によっては全く問題が起こらないこともあるかとは思いますが、別々のテストケースで同時に同じデータベースの同じテーブルを操作してしまった場合などを考えると、テストがコケうるのは想像に難くありません。 eftest において並列で実行したくないテストケースは ^:eftest/synchronized メタデータを付与することで、直列で実行することができるようにはなるのですが、元々並列で実行したいという話なので直列にしてしまっては意味がありません。

さて、問題はそれぞれのテストケースが同じ環境(データベース)を共有していることにあります。そういうことなら、全てのテストケースに対してユニークになるように環境を与えてやれば、この問題を解決できるということになります。 PostgreSQL を利用している場合、スキーマを利用することでテストケースごとの環境を用意することができるため、この問題を比較的カジュアルに解決できます。

任意のテストケースの開始時にスキーマを作成し、そのテストケースを抜けるタイミングでそのスキーマを破棄するようにすればよいため、 with-* のようなマクロを作ることにします。今回は with-test-database という名前にしましょう。実装は次のようになります。

(defmacro with-test-database [db-sym db-spec & body]
  (let [schema (rand-alphabet 10)]
    `(let [~db-sym (inject-schema ~db-spec ~schema)]
       (try
         (jdbc/execute! ~db-sym (str "create schema " ~schema))
         (migrate ~db-sym)

         [email protected]body

         (finally
           (jdbc/execute! ~db-sym (str "drop schema " ~schema " cascade")))))))

この実装の完全なコードは ここ にあります。

このマクロは第 2 引数として受け取ったデータベースの接続情報にスキーマの情報を加え、第 1 引数として渡した適当な名前でスキーマの情報が入った接続情報にアクセスすることができるようになっています。

今回はかなりチープな実装ですが、例えば Stuart Sierra’s Component や Integrant のようなライフサイクル管理ライブラリを利用していたとしても、同様の手法を用いることで並列化することが可能です(システムを起動する前の設定を書き換えたりすれば良い)。

というわけで簡単ですが、テストを並列に実行するテクニックの紹介でした。