これはなに
単に「HTTPステータスコードを確認するテスト」をPHPUnitで実装してローカルで動かしているよという記事です。
サンプルをGitHubのリポジトリ(phpunit-httpcheck)として公開しています。
要約
- PHPロジック多めの独自WordPressテーマをチューニングしたい
- 各ページがデグレしていないかリリース前に把握したい
- 用意したURLのリストに対してリクエストを発生させ、HTTPステータスコードを調べるPHPUnitのテストケースを作成した
- ローカルに加えてステージング・本番環境もテストできるようにした
- 検証するURLが多く時間がかかるためparatestで並列化
使用したライブラリはGuzzle、PHPUnit、paratest、phpdotenv
背景
リビンテクノロジーズで運営しているサイトの一部は、独自テーマを使ったWordPressで運用しています。
コンテンツで利用する各種データをAPIから取得するようには作れているのですが、テンプレートファイルの共通化やリクエストパラメーターの整理をする余地がある状態です。
最近は、このWordPressのサイトのパフォーマンス改善を行っています。
変更を加えた際に影響範囲を考慮できずに想定しなかったURLで不具合を発生させてしまうことがありました。
回帰テストを実行してエラーを事前に検知したいです。
仕様
前提条件
- リリースする前に検知したい
- 本番環境であれば監視ツールでHttpCheckを行いますが、開発中に気軽に実行できるようにローカルで実行できるようにしたいです
- DOMの確認はしなくてよい
- E2Eテストも将来的に行いたいところですが、まずはリクエストを発生させてエラーを検知できればよいとしました
- プログラミング言語は不問
- プロダクションコードを検査するわけではないため、プログラミング言語の縛りは特にないです。個人的には他の言語で書きたいところですが、同僚が使い慣れているPHPで実装することにします
要件のアイデア
- PHPUnitのテストコードとして表現できること
- Composerのscriptsによって実行できること
- ローカルに加え、ステージング環境と本番環境もチェックできること
実装
requirements
- PHP >= 7.4
- php.iniの
variables_order
にE
が含まれている- ※
$_ENV
を使うために必要
- ※
illuminate/testing 使おうとするもうまくいかない
Laravel初期化時に自動生成されるテストコードtest/Feature/ExampleTest.php
が、今回の実装アイデアに影響を与えています。
まず、これをそのままWordPressプロジェクトにも使えないかなと考えました。
EloquentやCollectionはLaravelではないPHPプロジェクトからでも使えるらしいことは知っていました。同じノリで composer require --dev illuminate/testing
して実装をしようと思うもautoloaderの設定がうまくいきませんでした。
今回の要件ではむしろシンプルに実装すればよいので深追いはしませんでした。
GuzzleでHTTPリクエストして検査
ライブラリをインストールします。
$ composer init
$ composer require --dev guzzlehttp/guzzle phpunit/phpunit
テストケースを書きます。このコンストラクタのシグネチャとparent
の呼出しはPhpstormが自動生成してくれました、便利。
<?php
declare(strict_types=1);
namespace Tests\Feature;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\GuzzleException;
use PHPUnit\Framework\TestCase;
class HttpCheckTest extends TestCase {
private Client $client;
public function __construct(?string $name = null, array $data = [], $dataName = '')
{
$this->client = new Client();
parent::__construct($name, $data, $dataName);
}
public function test_HTTPステータスが200(): void
{
$url = 'https://example.com';
try {
$response = $this->client->request('GET', $url);
$this->assertEquals(200, $response->getStatusCode());
} catch (ClientException $e) {
$this->fail("レスポンスエラー。url: '$url', statusCode: " . $e->getCode());
} catch (GuzzleException) {
$this->fail("通信エラー。url: '$url'");
}
}
実行します。
$ ./vendor/bin/phpunit tests/Feature/HttpCheckTest.php
PHPUnit 9.5.13 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 00:00.743, Memory: 6.00 MB
OK (1 test, 1 assertion)
URLを変更し、失敗することも確認します。例えば以下です。
- ステータスコード404を返すもの
https://example.com/404
- でたらめなもの
https://example
複数のURLに対応する
確認したいページ(パス)は複数あるため、それらを簡単にテストできるようにしたいです。
一般的なPHPアプリケーションだとControllerごとにテストを分けるのでしょうが、WordPressなので一括でHTTPテストをしてしまいます。
PHPUnitはパラメーターを変更して簡単にテストを行うためにDataProviderという仕組みがあります。
さっそくDataProviderを使って複数のURLに対応します。
+ /**
+ * @dataProvider urlProvider
+ */
- public function test_HTTPステータスが200(): void
+ public function test_HTTPステータスが200(string $url): void
{
- $url = 'https://example.com';
...略
}
+
+ public function urlProvider(): array
+ {
+ return [
+ ['https://example.com'],
+ ['https://example.com/404'],
+ ];
+ }
ファイル指定せずに実行できるようにする
ファイルを指定しなくても実行できるようにします。
ルートに phpunit.xml.dist
を追加すればOKです。
<?xml version="1.0"?>
<phpunit
backupGlobals="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
>
<testsuites>
<testsuite name="wordpress">
<directory suffix="Test.php">./tests/</directory>
</testsuite>
</testsuites>
</phpunit>
これにより、次のコマンドで実行できるようになりました。
$ ./vendor/bin/phpunit
composerコマンドから実行できるようにする
composer.jsonにエイリアスをはります。
{
"name": "lvn/lvnmatch_jp",
"type": "project",
"require-dev": {
"guzzlehttp/guzzle": "^7.4",
"phpunit/phpunit": "^9.5"
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"http": [
"phpunit"
]
}
}
次のコマンドで実行できるようになりました。
$ composer http
複数の環境に対応する
本番環境・ステージング環境・ローカル環境があります。
特にステージング環境にデプロイした際にも確認したいため、
それぞれのURLに対応するコマンドを実行することでテストできるようにします。
開発者によってローカル開発環境のURLが異なるため .env
で設定できるようにします。
.env
を作成します。同様の内容を.env.example
としてリポジトリにコミットしておくとよいです。
WordPressプロジェクト(Composerを使っていない)ため考慮する必要性は低いですが、通常の環境変数と区別できるように TEST_
というプレフィックスをつけてやります。
TEST_LOCAL_URL="http://localhost:8000"
.env
はgit管理からは除外します。
+ .env
テストコードから.env
ファイルを読めるようにします。
各環境でテストしたいパスは$basePaths
として扱えるようにします。
WordPressの記事などは環境によってURLが違うことがあります。環境特有のパスは $stagePaths
として扱えるようにします。
本番環境へのテストは負荷をかけないようにsleepをいれます。
なお本WordPressプロジェクトに関しては All-in-One WP Migration
でエクスポートした本番環境のデータをローカル環境で使っているため、本番とローカルの記事のパスは同じです。
$ composer require --dev vlucas/phpdotenv
class HttpCheckTest extends TestCase {
private Client $client;
public function __construct(?string $name = null, array $data = [], $dataName = '')
{
+ $dotenv = \Dotenv\Dotenv::createImmutable(__DIR__ . '/../../');
+ $dotenv->load();
$this->client = new Client();
parent::__construct($name, $data, $dataName);
}
/**
* @dataProvider urlProvider
*/
public function test_HTTPステータスが200(string $url)
{
+ if($_ENV['TEST_ENV'] === 'production') {
+ sleep(1);
+ }
...略
}
public function urlProvider(): array
{
- return [
- ['https://example.com'],
- ];
+ $env = $_ENV['TEST_ENV'];
+ if ($env === 'production' || $env === 'local') {
+ if ($env === 'production') {
+ $url = 'https://本番環境のURL';
+ } else {
+ $url = $_ENV['TEST_LOCAL_URL'];
+ }
+ $stagePaths = [];
+ } else if ($env === 'staging') {
+ $url = 'https://ステージング環境のURL';
+ $stagePaths = [];
+ } else {
+ return []; // skip test.
+ }
+ $basePaths = [
+ '/',
+ '/hoge',
+ '/fuga',
+ ];
+ $paths = [...$stagePaths, ...$basePaths];
+ return array_map(fn($path) => [$url . $path], $paths);
}
"scripts": {
- "http": [
- "phpunit"
+ "http:prod": [
+ "TEST_ENV=production phpunit"
+ ],
+ "http:stg": [
+ "TEST_ENV=staging phpunit"
+ ],
+ "http:dev": [
+ "TEST_ENV=local phpunit"
]
}
テストしたい環境のコマンドを実行できるようになりました。
コロンに違和感があるかもしれませんがnpmのscriptsでよく見る書き方を真似しています。
$ composer http:prod
$ composer http:stg
$ composer http:dev
paratestで並列実行する
テストケースを直列で実行しているためURLの数が多いと時間がかかります。paratestを使って並列で行えるように変更します。
まずはparatestを依存に加えます。
$ composer require --dev brianium/paratest
phpdotenvをcreateMutable()
に変更します。
public function __construct(?string $name = null, array $data = [], $dataName = '')
{
- $dotenv = \Dotenv\Dotenv::createImmutable(__DIR__ . '/../../');
+ $dotenv = \Dotenv\Dotenv::createMutable(__DIR__ . '/../../');
$dotenv->load();
$this->client = new Client();
parent::__construct($name, $data, $dataName);
}
テストするURLを試しに30個に増やして挙動を確認します。
例としてurlPrivider()
が提供するURLをhttps://google.com/
にして検証する場合は次のように関数を変更します。
※実際は負荷をかけても問題のないURLで実行してください。
public function urlProvider(): array
{
return array_map(fn() => ['https://google.com/'], range(1, 30));
}
実行します。テストケースが少なく、dataProviderを使ったテストをしているため-f
オプションを指定する点がミソです。
$ ./vendor/bin/paratest -p 4 -f
4プロセスで実行されます。テストケースにsleep(1);
をはさむと1秒おきに4つずつテストが進むことを目視できます。
本番環境以外のComposerのscriptsをparatestのものに置き換えます。
{
"scripts": {
"http:prod": [
"TEST_ENV=production phpunit"
],
"http:stg": [
"TEST_ENV=staging paratest -p 4 -f"
],
"http:dev": [
"TEST_ENV=local paratest -p 4 -f"
]
}
}
これで求めているものが実装できました。
urlPrivider()
はもちろん元に戻します。
運用
実行するタイミング
ローカル環境用のテストケースを実行するタイミングは開発中ですが、
少なくともプルリクエストのレビュー依頼前に1度実行するようにします。
エラーを確認する仕組みを作る
本テストケースでは、不具合のあったURLはわかりますがエラーの内容まではわかりません。
ローカルからの実行でもメールやSlack、Sentryなどにエラー内容を通知できるようにして対応を簡単にします。
テストケースにURLを追加する
次のような場合はテストケースにURLを追加します。
- 配列に用意していないURLでエラーが発生した場合
- エラーが関数に関係するものならユニットテストを書きます。
- ユニットテストで対応できない時はそのURLを今回のテストケースに追加します。
- ページ(URL)が増えた場合
おわりに
アプリケーションへ変更を加えた後に、簡単なものですがデグレを検知する仕組みを構築できました。
検知したエラーはSentryに通知して対応できるようにしています。
以前よりも安心してリリースできて体験がよい!