fixturize: テストの実行速度を大幅に改善させるプラグイン

Posted on
Plugins

※1人AdventのDay-22です

1人advent(CakePHP中心、PHP開発よもやま) Advent Calendar 2018 - Adventar


概要

friendsofcake/fixturizeは、CakePHPにおける単体テストの実行速度を大幅に改善させるプラグインです。
MySQL互換RDBMSで利用が可能で、導入は簡単です。
実際の利用方法と

イントロ

CakePHPのTestSuiteでは、非常に簡単にDBと連携したフィクスチャデータを利用することができます。
予めPHPクラスとして記述しておいたスキーマやレコードの内容を、必要に応じて指定した通りに読み込む形です。
レコードの作成作業は、各テストケースの実行時に行われます。

このライフサイクルは直感的でクリーンさを保ちやすい一方で、懸念となるのは実行速度です。1つのテストケースごとに、毎回「テーブルの中身の破棄」「完全なデータのインサート」が行われるからです。

そうしたオーバーヘッドを削減するために作り出されたプラグインが、fixturizeです。

fixturizeの概要

FriendsOfCake/fixturize: CakePHP3: Improve performance of your fixture based tests on MySQL.

fixturizeは、既存の TestFixture クラスを当プラグインのクラスに置き換えることで実行されます。
通常のTestFixtureの処理をオーバーライドする形で、

  1. テストケース実行時(setUp)に
  2. 通常は「テーブルのトランケートを行う」ところで
  3. もし「今作成済みの対象テーブルの内容が、fixtureに記述された内容と差異がない」ことを確認できたら
  4. truncate/insert処理をスキップする

という機能を追加します。
そのために、「fixtureデータの作成・破棄・更新」に係るコストが省略されるわけです。
この結果、レポジトリにあるREADMEの内容を参照すると大変な時間短縮の成果を生み出しています。

導入

導入はとてもシンプルです。
READMEの内容をそのまま参照します。

  1. composerを利用してインストールする
    • composer require friendsofcake/fixturize
  2. 既存のfixtureの、継承元クラスを変更する
    • もちろん、extends CheckSumTestFixture をしても良いと思いますが、importを書き換えてaliasを当てる〜とした方が手間が少ないです

導入はこれだけで完了です。

実装詳細

それでは、どのように「無駄な更新はスキップする!」を実現しているのでしょうか。
具体的な実装内容について確認していきます。

ChecksumTestFixtureがオーバーライドしているメソッドは、 insert() truncate() drop() の3つのメソッドです。
これらの内部において、このプラグインの機能である「更新する必要があるか」をチェックできるように処理が追加されています。

テストのブート時やテストケースクラスのブート時にもfixture関連の処理が入ります。
しかし、fixturizeの機能は主に「反復的にfixtureのセットアップをする際に、どのような挙動をするか」という点にあるといえるでしょう。
そのため、今回は要点を描きやすくするために「各テストケース間の処理」について掻い摘んで流れを追っていきます。

Fixtureの処理の流れ

TestCaseクラスが、FixtureManagerクラス経由で load()処理を呼ぶことになります。
insert() はその内部で実行されるものです。
詳細は省きますが1、各テストケースの前に insert() 、次のテストの実行前にtruncate() が実行されるものとイメージして問題ありません。 FixtureManagerを使役するFixtureInjectorクラスには、startTest() endTest() という名前のメソッドがありまして、その内部でそれぞれのメソッドが呼ばれている・・というと、イメージを掴む上での一助となるでしょうか。

ものすごく端折った説明をすると、以下のような流れになります。

このシーケンスの中で、具体的な処理が行われているポイントにChecksumTestFixtureが介入するわけです。

「テーブルの中身が同じ」であることの判定方法

前提として、そもそも「テーブルの中身がさっきと同じ」というのはどのように判定しているのでしょうか?

これには、MySQLのCHECKSUM を利用しています。
以下の記事などを参考にしてください。

第68回 MySQLにおけるデータの比較:MySQL道普請便り|gihyo.jp … 技術評論社

これによって、「Aというテーブルが、今の状態はxxxなんだけど、さっきもxxxだった?」という確認をTestFixtureクラスが実行可能としているのです。

_tableUnmodified()メソッド

その「テーブルごとの比較」を行っている具体的な実装が、 ChecksumTestFixture::_tableUnmodified()です。
これは、

  1. fixtureごとのチェックサムを取得し
  2. 静的クラスメンバーとして、fixtureごとの最新のチェックサムを都度保持し
  3. 「今とってきたチェックサム」と「クラスが保持しているチェックサム」の比較を行っています。

もし、「チェックサムが合致しない」ということになれば、それは「データが更新されている」ということになります。すなわち、「fixtureクラスに定義されている状態に戻して上げる必要がある」と判断できるわけです。

既存メソッドをチェックサム連動用にオーバーライド

比較するための機構は持てたので、あとは「都度チェックサムを記録する」「都度チェックサムを確認する」ように、既存メソッドを拡張できればOKです。

  • insert()メソッド
    • fixtureからdbにデータを流し込むメソッド
    • もし、もともとのデータ(=fixtureクラスで定義されたデータ)と今あるデータが合致していれば、「流し込む必要がない」といえる
      • _tableUnmodified()がtrueならtruncate処理をスキップ
    • (データの更新が検知されて)fixtureクラスで定義されたデータを流し込んだ場合、クラスメンバーの自身に関するチェックサムを更新する
  • truncate()メソッド
    • tableの内容を空にする(リセットする)メソッド
    • このメソッドが呼び出されるたびに、本当に「リセットしてしまう」と、「さっきのデータをそのまま使う」処理が不能になる
    • そのため、もし「書き換えが発生していない」と判断できたら、実際にtruncateさせずにスルーさせる必要がある
      • _tableUnmodified()がtrueならtruncate処理をスキップ
  • drop()メソッド
    • tableを削除するメソッド
    • 呼び出されたタイミングで、保持していたチェックサムをunsetする
      • = 当該テーブルに関する _tableUnmodified()が参照する比較対象が不在になる = 確実にfalseを返すようになる
      • 本来の処理に戻す

このような仕組みで、fixtureのセットアップに関するオーバーヘッドを大幅に削減しています。

既知の問題

複数のdbで同名のテーブルを利用した際などに、同一チェックが正常に動かない問題があります。
この問題については、過去にパッチを作成して報告済みです。
具体的な問題や対応について、ご興味を持たれましたらこちらを覗いてみていただければと思います。

Make it possible to use different fixtures for same tables. by o0h · Pull Request #8 · FriendsOfCake/fixturize


  1. いずれ、テスト実行のライフサイクルについては詳細に追ってみたいなという気持ちがあります。 [return]