レガシーPHPをサポートしたTestcontainers作りました
テストに便利なTestcontainersのPHP実装を作りました。
本記事ではこちらのライブラリの紹介をします。
モチベーション
TestcontainersのPHP実装としては公式から提供されているtestcontainers/testcontainersが存在します。
PHPでTestcontainersを利用したい場合には基本的にこちらを使うのが正解です。
しかし、1つ難点があり、PHP v8.1以降しかサポートしていません。
2025/03現在、PHPのサポートバージョンは8.1以降です。
そのためライブラリとして8.1以降をサポートしているというのは正しい選択肢ではあるのですが・・・怒らないので正直に8.1未満を利用している方は手を挙げてみてください。そう、いろいろな諸事情により8.1未満を利用していることはよくあると思います。(よくあっては欲しくないですが)
8.1未満の方は早急にPHPのバージョンアップをしなければなりません。
しかし、バージョンアップするためにはテストが重要であり、テストを書くのにTestcontainersを使えないというのは厄介です。
というので、この隙間を埋めるためにPHP v5.6以降をサポートしたTestcontaners実装を作成しました。
(本来、この手のことをするには本家にPRを出すべきですが、さすがにPHPのサポートしていないバージョンをサポートしましょうという勇気はなかったです)
設計思想
とにかくEasyになるように作成しています。
ライブラリを作成するさいにEasyに寄りすぎると無駄に機能が増え厄介なのですが、今回はレガシーな環境をターゲットにしているため限りなく簡単に使って効果を出せるようにしています。
例えばコンテナ定義の仕方では3種類用意してます。
// Static Property
class MyContainer extends GenericContainer
{
protected static $STARTUP_TIMEOUT = 60; // seconds
protected static $STARTUP_CHECK_STRATEGY = 'is_running';
}
// Method Override
class MyContainer extends GenericContainer
{
protected function startupTimeout()
{
return 60; // seconds
}
protected function startupCheckStrategy()
{
return new \Testcontainers\Containers\StartupCheckStrategy\IsRunningStartupCheckStrategy();
}
}
// Fluent API
$container = (new GenericContainer('nginx:latest'))
->withStartupTimeout(60)
->withStartupCheckStrategy(
new \Testcontainers\Containers\StartupCheckStrategy\IsRunningStartupCheckStrategy()
);
1つで十分な気もしますが、レガシー環境に合わせてこういうことをやりたい・・・みたいなのがあるさいに、どうにでもできるようにしています。
また、同じ設定でもいくつかエイリアスを持たせるようにしています。
class MyContainer extends GenericContainer
{
protected static $EXPOSED_PORTS = [80];
protected static $EXPOSE = [80];
protected static $PORTS = [80];
}
本家的にはEXPOSED_PORTS
が正しいのですが、何度か$PORTS
と打ち間違えた時点で直感的じゃないやと複数の名前をサポートするようにしています。
他にも実際に利用するさいにエラーが発生してよくわからない・・・と、なりがちなのでログ出力をサポートしたり、エラーメッセージを細かくしたり、無駄に優しさを出すように意識しています。
もし、優しさが足りないと思ったらIssueに上げてください。優しさを追加します。
基本機能
k-kinzal/testcontainers-phpの使い方は簡単です。
まず、動かしたいコンテナを定義します。
class NginxContainer extends GenericContainer
{
protected static $IMAGE = 'nginx:latest';
protected static $EXPOSED_PORTS = [80, 443];
protected function waitStrategy()
{
return (new HttpWaitStrategy())
->withPort(80)
->withPath('/')
->withExpectedResponseCode(200)
->withTimeoutSeconds(30);
}
}
次にテストケースでコンテナを起動します。
class MyTest extends TestCase
{
public function test()
{
Testcontainers::run(NginxContainer::class);
// your test case
}
}
これだけでテスト中にコンテナが起動し、テストが終了したら良い感じにコンテナが停止します。
簡単ですね!!
本家にない機能
CircleCIのDocker ExecutorサポートとしてSSHポートフォワーディングをサポートしています。
これはCircleCIのDocker Executor環境ではDockerがリモート実行になり、リモートのポートが空いていないため、立ち上げたコンテナにアクセスができません。
唯一空いているのはremote-docker
の22番でSSHを経由させる必要があります。
PHPUnitで実行のさいに小細工を入れても良いのですが、そうするとローカルとCIの複数環境で動作しない、または複雑な動きになってしまいます。
version: 2.1
jobs:
test:
docker:
- image: cimg/php:8.1
steps:
- checkout
- setup_remote_docker:
version: 20.10.14
- run:
name: Run tests
environment:
TESTCONTAINERS_SSH_FEEDFORWARDING: remote-docker
command: vendor/bin/phpunit
そこで、k-kinzal/testcontainers-phpとしては上記のようにTESTCONTAINERS_SSH_FEEDFORWARDING
を設定するとSSHポートフォワーディングが動作するような仕組みを追加しています。
今後の予定
基本的にはテストをちまちま追加&実運用しながらバグフィックスを予定しています。
ただ、本家の機能でいくつかサポートできてない機能があるので、そのあたりも気が向いたら作成する予定です。
- Testcontainers Cloudサポート(結構大きな改修が必要そう)
- 起動後のdocker cpでのファイルコピーのサポート
- 複数コンテナの依存のサポート
また、本家にはないですが、このあたりあると良さそうとも思ったので気が向いたらちまちま追加していきたい気持ちです。
- コンテナ起動前のビルドサポート
- レジストリの認証の絡みで厄介だったのでいっそビルドした方が早いのでは説が出たので
- Dockerfileの直書きサポート
- Docker APIの各種コマンドサポート
- 現状はTestcontainersに必要なものしか作っていませんが、あると便利そうな匂いがしたので
- ゼロ依存化
- さらに古いPHPのサポート
- PHP v8.4のサポート
それはそうとしてレガシーコード調査・分析用にPHPコードの依存グラフを生成するツールも作りたい!!みたいなのあるので無限に時間欲しいです。(たぶん、これができるとLLMがレガシーコード扱いやすくなる目算)
作っていて難しかったところ
基本的にTestcontainersに必要な処理というのは少ないので、基本動作としては作るのそんなに難しくはなかったです。
ただ、厄介だったのがGenericContainer
の機能が本当に多く、肥大化しすぎて管理しきれない・・となったのは地味に困りました。
最終的にTraitで分離して何とかしましたが、これ古いPHPサポートの障害になっているのでどうしたものやらとなっています。
また、地味に困ったのは令和の時代にPHP v5.6のバグを踏みました。
詳細はこちらに書いているので興味ある方はご覧ください。あとは古いPHPに合わせて書いているので地味に縛りプレイになっていて虚無に襲われる瞬間があります。
あれ・・・?今は令和・・・?令和の時代に僕は何を書いているんだ・・・????
おわりに
皆さんにこれだけは伝えたいです。
PHPのサポートバージョンを使いましょう!!!!!
そして本家のTestcontainersを使って楽しいテスト生活を送りましょう!!!!!!
Discussion