
(将棋ソシャゲ)PHP でテスト駆動開発ができるようにする。 ~ PHPUnit ~
こんばんは。将棋ソシャゲ開発運営室の九条です。
今日は、PHPUnit を使って、テスト駆動開発の環境を構築したので、私自身の忘備録として、またチームメンバー間でやり方を共有するため、その方法を解説します。
この設定を終えると、コマンドラインから PHP のクラスレベル単体テストができるようになります。
前提条件
以下の環境にて、デバッグ実行できるようにします。他のバージョンでも、ほぼ同じ手順で動作すると思われます。
Windows
PHP 8.2.5
次の記事も参照してください。
本記事では、テスト駆動開発の技法そのものについては解説しません。これについては、既に知っているという前提で解説します。
PHPUnit のセットアップ
PHP でテスト駆動開発をするには、PHPUnit というフレームワークの導入が必要です。
Composer の導入
まず、PHPUnit を使うには、前提として、Composer を導入しなければなりません。Composer はそれ自体はライブラリやフレームワークではなく、「パッケージマネージャ」と呼ばれるソフトです。
Composer の導入については、このサイトを参考にしました。
なお、Composer は、PHP のエクステンションの「curl」と「zip」を使うので、Composer のインストールをする前に、予め有効にしておいた方が良いでしょう。「curl」は、Composer をインストールする際に、インストーラが勝手に php.ini を編集して有効にしてくれるのですが、「zip」は何故か有効にしてくれないので、後で戸惑う可能性があります。
extension=curl
extension=zip
Composer をインストールすると、Composer は、php.ini をインストールに際して、勝手に編集するので、バックアップが作成されます。WinMerge 等の差分比較ツールを使って、編集前と編集後の差分をチェックすると確実だと思います。編集前のファイルは「php.ini~orig」というファイルにバックアップされます。
PHPUnit の導入
コマンドプロンプトを開き、「cd」コマンドで、カレントディレクトリを、プロジェクトディレクトリ(この場合は、将棋ソシャゲのファイル一式が置かれているディレクトリ)に移動します。
私の環境ではこうなります。
cd C:\Apache24\htdocs\shogi-soshage
次に、Composer コマンドを実行し、PHPUnit をインストールします。
composer require --dev phpunit/phpunit
プロジェクトの設定
次のような、PHPUnit の設定ファイルをプロジェクトディレクトリに作成します。ファイル名は「phpunit.xml」とします。
<phpunit bootstrap="vendor/autoload.php">
<testsuites>
<testsuite name="Application Test Suite">
<directory>./tests</directory>
</testsuite>
</testsuites>
</phpunit>
ディレクトリ構成
製品コード(プロダクトコード)と、テストコードは、分ける方が望ましいです。
AI(ChatGPT)は次のようなディレクトリ構成を提案してきました。
project/
├── src/ // アプリケーションコード
├── tests/ // テストコード
├── vendor/ // Composerの依存関係
└── phpunit.xml // PHPUnitの設定ファイル
tests/ と vendor/ と phpunit.xml は新規に追加するものなので良いとして、src/ にプロダクトコードを入れるためには、既存のディレクトリ構成を変更しなければならず、この変更は危険があるため、したくありませんでした。
そこで、プロダクトコードは引き続き、プロジェクトディレクトリの直下に置き続けることにして、tests/ と phpunit.xml だけを新規に作成しました。
※vendor/ は Composer が自動的に生成します。
バージョン管理上の注意点
Composer から PHPUnit を導入した時点で、次のディレクトリとファイルが自動生成されます。
vendor/
composer.json
composer.lock
これらは、バージョン管理下に置くべきでしょうか?バージョン管理下から除外すべきでしょうか?
AI(ChatGPT)は次の通り回答してきました。
vendor/ は管理下から除外すべき。
composer.json と composer.lock は、管理下に置くべき。
vendor/ は、ダウンロードしたライブラリやフレームワークのソースコード本体が配置されるディレクトリであり、開発者が各自管理すべきです。
composer.json と composer.lock は、パッケージのバージョンや、依存関係が記述されています。これをバージョン管理下に置いておけば、他の開発者は次のコマンドを実行するだけで、私と同じ環境を、即座に再現できるそうです。
composer install
しかし、私は、全部バージョン管理下から除外しました。
というのは、開発者間で、PHP のバージョンすら統一されておらず、却って混乱のもとになる可能性があると考えたためです。
「.gitignore」ファイルに次の3行を追加しました。
vendor/
composer.json
composer.lock
composer init について
情報源によっては、PHPUnit のインストール前に「composer init」コマンドを実行している物もあります。
このコマンドは、プロジェクトの初期設定を行うためのコマンドで、「composer.json」ファイルを対話式に自動で生成してくれます。
結論から言うと、これは必須ではありません。
「composer.json」ファイルが存在しない状態で、「composer require --dev phpunit/phpunit」コマンドを実行したところ、「composer.json」ファイルが自動的に生成されます。
AI(ChatGPT)によれば、このまま進めて問題ないそうです。
PHPUnit によるテストの実行
サンプルコードの作成
まず、簡単なサンプルを作成します。
Calculator.php
<?php
class Calculator
{
public function add($a, $b)
{
return $a + $b;
}
}
tests/CalculatorTest.php
<?php
use PHPUnit\Framework\TestCase;
require_once __DIR__ . '/../Calculator.php';
class CalculatorTest extends TestCase
{
public function testAddition()
{
$calculator = new Calculator();
$result = $calculator->add(2, 3);
$this->assertEquals(5, $result);
}
}
なお、AI(ChatGPT)が提示したコードには「require_once __DIR__ . '/../Calculator.php';」の1行が存在しませんでしたが、これが無いと、エラーになります。
テストの実行
テストを実行するには、次のコマンドを実行します。
./vendor/bin/phpunit
結果は、コマンドラインに出力されます。上記サンプルをコピペしてあれば、テストは成功するはずです。
テストを実行すると、次のような警告が表示されるかもしれません。
「This test does not define a code coverage target but is expected to do so」
これは、カバレッジツールのための属性の指定が無いことを示しています。
これについては、次回の記事でカバレッジツールの導入について解説する際に、解決します。
バージョン管理上の注意(再)
テストを実行すると、「.phpunit.cache/」フォルダが生成されますが、これはバージョン管理下から除外すべきです。
「.gitignore」ファイルに次の1行を追加しました。
.phpunit.cache/
命名規約
テストコードのクラス名、関数名に命名規約があるので注意が必要です。
ファイル名は自由だが、末尾を「~Test.php」にすることが一般的。
クラス名は、末尾を「~Test」としなければならない。
メソッド名は、先頭を「test~」としなければならない。
当然だが、テストコードのファイルは「tests/」に配置する。
なお、テストメソッドに、「@test」アノテーションを付けることで、先頭を「test~」としていないメソッドでも、テストメソッドとして認識させることが可能です。(下記テストコードを参照。)
FunctionsTest.php
<?php
use PHPUnit\Framework\TestCase;
require_once __DIR__ . '/../Calculator.php';
class CalculatorTest extends TestCase
{
/**
* @test
*/
public function testAddition()
{
$calculator = new Calculator();
$result = $calculator->add(2, 3);
$this->assertEquals(5, $result);
}
}
NUnit の [TestCase] 属性相当品(@dataProvider)
例えば、NUnit(C#/.NET のテスティングフレームワーク)では、次のように書くことで、1つのテストメソッドで、テストデータを切り替えながら、複数回のテスト試行を行うことが可能です。
[TestCase(2, 3, 5)]
[TestCase(1, 1, 2)]
public void AddTest(int a, int b, int expected)
{
Assert.AreEqual(expected, Add(a, b));
}
同様の書き方が、PHPUnit でもできれば便利です。
PHPUnit で同じようなことをするには、こう書きます。
<?php
use PHPUnit\Framework\TestCase;
class AddFunctionTest extends TestCase
{
/**
* @dataProvider additionProvider
*/
public function testAdd($a, $b, $expected)
{
$this->assertEquals($expected, add($a, $b));
}
public static function additionProvider()
{
return [
[2, 3, 5],
[1, 1, 2],
[0, 0, 0],
];
}
}
次の5つがポイントです。
テストメソッドに引数を追加する。
テストメソッドに「@dataProvider」アノテーションを付加する。
「@dataProvider」アノテーションで指定された名前の関数を用意する。
上記関数を、必ず static に指定する。
上記関数で、テストデータを配列として返す。
実は、4つ目の static 指定をしていないと、テストを実行してくれなくなるのですが、AI(ChatGPT)の回答に誤りがあり、AI(ChatGPT)の回答では、static 指定されていませんでした。これでだいぶハマりました。
テストケースに名前を付けて、コマンドラインに表示されるテスト結果を分かりやすくすることも可能です。
public function additionProvider()
{
return [
'case1: 2+3=5' => [2, 3, 5],
'case2: 1+1=2' => [1, 1, 2],
'case3: 0+0=0' => [0, 0, 0],
];
}
将棋ソシャゲのテストケース
将棋ソシャゲには、グローバル関数として、数値データを、漢数字の文字列に変換する関数があります。これは、盤面のヘッダー部分の段を表示するのに使っています。
これに対して、次のようなテストを書きました。
<?php
require_once __DIR__ . '/../functions.php';
use PHPUnit\Framework\TestCase;
class FunctionsTest extends TestCase
{
/**
* @test
* @dataProvider numberToKanjiProvider
*/
public function test_numberToKanji_01($args, $expected)
{
$actual = numberToKanji($args);
$this->assertSame($expected, $actual);
}
public static function numberToKanjiProvider()
{
$data = [
'-1=>NULL' => [-1, NULL],
'0=>零' => [0, '零'],
'1=>一' => [1, '一'],
'9=>九' => [9, '九'],
'10=>十' => [10, '十'],
'11=>十一' => [11, '十一'],
'19=>十九' => [19, '十九'],
'20=>二十' => [20, '二十'],
'21=>二一' => [21, '二一'],
'29=>二九' => [29, '二九'],
'99=>九九' => [99, '九九'],
'100=>NULL' => [100, NULL],
];
return $data;
}
}
?>
これは最終成果物だけを示していますが、製品コード(プロダクトコード)がすでに完成している場合は、「仕様化テスト」という技法を使います。
「仕様化テスト」では、①わざと失敗するテストコードを書く。②テストが成功するようにテストコードを修正する。という、①と②の手順を繰り返します。
このテストを実行すると、テストは実行できますが、次のメッセージが表示されます。
There was 1 PHPUnit test runner deprecation:
1) Metadata found in doc-comment for method FunctionsTest::test_numberToKanji_01(). Metadata in doc-comments is deprecated and will no longer be supported in PHPUnit 12. Update your test code to use attributes instead.
OK, but there were issues!
Tests: 12, Assertions: 12, PHPUnit Deprecations: 1.
どうやら、アノテーションを使う書き方は、最新の PHPUnit では、非推奨になっているらしく、それでメッセージを表示してきます。最新の PHPUnit では、アノテーションの代わりに、「属性」を使う必要があるそうです。
実は、私は、アノテーションと属性の違いが分かっていなかったのですが、別の文法らしいです。
これについては、私の環境では未解決なので、今後調査していきたいと思います。
assertEquals() と assertSame() の違い
アサーションに使う「assertEquals()」関数は、内部的に「==」で等価判定をしています。一方、「===」で等価判定をするには、「assertSame()」関数を使用します。
上記の「numberToKanji()」関数のテストでは、NULL との比較が必要になるので、「assertSame()」関数を使用しています。
最後に
今回は、ChatGPT の支援も受けながらやりました。ChatGPT とのやり取りを示しておきます。
今回は以上です。次回は、PHPUnit を使って、ビジュアルなテストレポートを作成する方法、及びカバレッジツールを使う方法を解説します。