PHPUnit@PHPユニットテスト¶
はじめに¶
本サイトにつきまして、以下をご認識のほど宜しくお願いいたします。
01. PHPUnitとは¶
ユニットテストと機能テストの実施に必要な機能を提供し、加えてテストを実施する。
02. コマンド¶
オプション無し¶
全てのテストファイルを対象として、定義されたメソッドを実行する。
$ vendor/bin/phpunit
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.
... 3 / 3 (100%)
Time: 621 ms, Memory: 24.00 MB
OK (3 tests, 3 assertions)
--filter¶
特定のテストファイルを対象として、定義されたメソッドを実行する。
$ vendor/bin/phpunit --filter Foo
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.
... 1 / 1 (100%)
Time: 207 ms, Memory: 8.00 MB
OK (1 tests, 1 assertions)
--list-tests¶
テストファイルの一覧を取得する。
$ vendor/bin/phpunit --list-tests
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.
Available test(s):
- Tests\Unit\FooTest::testFooMethod
- Tests\Feature\FooTest::testFooMethod
03. phpunit.xmlファイル¶
phpunit.xml
ファイルとは¶
PHPUnitの設定を実行する。
デフォルトの設定では、あらかじめルートディレクトリにtests
ディレクトリを配置し、これをUnits
ディレクトリまたはFeature
ディレクトリに分割しておく。
また、Test
で終了するphpファイルを作成しておく必要がある。
testsuites
タグ¶
テストスイートを定義できる。
testsuites
タグ内のtestsuites
タグを追加変更すると、検証対象のディレクトリを増やし、加えて対象のディレクトリ名を変更できる。
<phpunit>
...
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
</testsuites>
...
</phpunit>
php
タグ¶
PHPUnitの実行前に設定するini_set
関数、define
関数、グローバル変数、を定義できる。
タグ名との対応関係については、以下のリンクを参考にせよ。
*実装例*
Composerの実行時にメモリ不足にならないようにメモリを拡張する。
また、テスト用のDBに通信できるよう、DBに関する環境変数を設定する。
<phpunit>
...
<php>
<!-- <グローバル変数名 name="キー名" value="値"/> -->
<!-- Composerの実行時にメモリ不足にならないようにする -->
<ini name="memory_limit" value="512M"/>
<!-- DBの接続情報 -->
<server name="DB_CONNECTION" value="mysql"/>
<server name="DB_DATABASE" value="test"/>
<server name="DB_USERNAME" value="test"/>
<server name="DB_PASSWORD" value="test"/>
</php>
...
</phpunit>
04. アサーションメソッド¶
アサーションメソッドとは¶
実測値と期待値を比較し、結果に応じてSUCCESS
またはFAILURES
を返却する。
非staticまたはstaticとしてコールできる。
$this->assertTrue();
self::assertTrue()
assertTrue¶
実際値がtrue
か否かを検証する。
$this->assertTrue($response->isOk());
assertEquals¶
『==
』を使用して、期待値と実際値の整合性を検証する。
データ型を検証できないため、assertSame
メソッドを使用する方が良い。
$this->assertSame(200, $response->getStatusCode());
assertSame¶
『===
』を使用して、期待値と実際値の整合性を検証する。
値のみでなく、データ型も検証できる。
$this->assertSame(200, $response->getStatusCode());
05. テストデータ¶
Data Provider¶
テスト対象のメソッドの引数を事前に用意する。
メソッドのアノテーションで、@test
と@dataProvider データプロバイダ名
を宣言する。
データプロバイダの返却値として配列を設定し、配列の値の順番で、引数に値を渡せる。
*実装例*
<?php
use PHPUnit\Framework\TestCase;
class FooTest extends TestCase
{
/**
* findメソッドをテストする
*
* @test
* @dataProvider methodDataProvider
*/
public function testFind_Bar_Baz($paramA, $paramB, $paramC)
{
// 何らかの処理
}
/**
* findメソッドを引数を用意する
*
* @return array
*/
public function methodDataProvider(): array
{
return [
// 配列データは複数あっても良い、
["1", "2", "3"]
];
}
}
06. 事前処理と事後処理¶
setUp
メソッド¶
事前処理として、全てのテスト関数の前にコールされるメソッドである。
*実装例*
DIコンテナを事前に作成する。
<?php
use PHPUnit\Framework\TestCase;
class FooTest extends TestCase
{
protected $container;
// 全てのテスト関数の前に実行される。
protected function setUp()
{
// DIコンテナにデータを格納する。
$this->container["option"];
}
}
*実装例*
ユニットテストで検証するクラスが実際の処理の中でインスタンス化される時、依存先のクラスはすでにインスタンス化されているはずである。
そのため、これと同様に依存先のクラスのモックを事前に作成しておく。
<?php
use PHPUnit\Framework\TestCase;
class FooTest extends TestCase
{
protected $foo;
protected function setUp()
{
// 基本的には、一番最初に記述する。
parent::setUp();
// 事前にモックを作成しておく。
$this->bar = Phake::mock(Bar::class);
}
public function testFoo_Xxx_Xxx()
{
// 実際の処理では、インスタンス化時に、FooクラスはBarクラスに依存している。
$foo = new Foo($this->bar)
// 何らかのテストコード
}
}
tearDown
メソッド¶
事後処理として、全てのテスト関数の後にコールされるメソッドである。
グローバル変数やサービスコンテナにデータを格納する場合、後の検証でもそのデータが誤って使用されてしまわないように、サービスコンテナを破棄するために使用される。
*実装例*
<?php
use PHPUnit\Framework\TestCase;
class FooTest extends TestCase
{
protected $container;
protected function setUp()
{
$this->container["option"];
}
// 全てのテスト関数の後に実行される。
protected function tearDown()
{
// DIコンテナにnullを格納する。
$this->container = null;
}
}
07. テストダブル¶
createMock
メソッド¶
クラスの名前空間を元に、モックまたはスタブとして使用する擬似オブジェクトを作成する。
以降の処理での用途によって、呼び名が異なることに注意する。
補足として、PHPUnitの場合、モックのメソッドはnull
を返却する。
<?php
use PHPUnit\Framework\TestCase;
class FooTest extends TestCase
{
/**
* @test
*/
public function testFoo()
{
// モックとして使用する擬似オブジェクトを作成する。
$mock = $this->createMock(Foo::class);
// null
$foo = $mock->find(1)
}
}
<?php
class Foo
{
/**
* @param int
* @return array
*/
public function find(int $id)
{
// 参照する処理
}
}
method
メソッド¶
モックまたはスタブのメソッドに対して、処理の内容を定義する。
特定の変数が渡された時に、特定の値を返却させられる。
<?php
use PHPUnit\Framework\TestCase;
class FooTest extends TestCase
{
/**
* @test
*/
public function testFoo_Xxx_Xxx()
{
// スタブとして使用する擬似オブジェクトを作成する。
$stub = $this->createMock(Foo::class);
// スタブのメソッドに処理内容を定義する。
$stub->method("find")
->with(1)
->willReturn([]);
// [] (空配列)
$result = $stub->find(1)
}
}
08. テストケース¶
ユニットテストの場合¶
*実装例*
以降のテストケースでは、次のような通知クラスとメッセージクラスが前提にあるとする。
<?php
use CouldNotSendMessageException;
class FooNotification
{
private $httpClient;
private $token;
private $logger;
public function __construct(Client $httpClient, string $token, LoggerInterface $logger)
{
$this->httpClient = $httpClient;
$this->token = $token;
$this->logger = $logger;
}
public function sendMessage(FooMessage $fooMessage)
{
if (empty($this->token)) {
throw new CouldNotSendMessageException("API requests is required.");
}
if (empty($fooMessage->channel_id)) {
throw new CouldNotSendMessageException("Channel ID is required.");
}
$json = json_encode($fooMessage->message);
try {
$this->httpClient->request(
"POST",
"https://example.com",
[
"headers" => [
"Authorization" => $this->token,
"Content-Length" => strlen($json),
"Content-Type" => "application/json",
],
"form_params" => [
"body" => $fooMessage->message
]
]
);
} catch (ClientException $exception) {
$this->logger->error(sprintf(
"ERROR: %s at %s line %s",
$exception->getMessage(),
$exception->getFile(),
$exception->getLine()
));
throw new CouldNotSendMessageException($exception->getMessage());
} catch (\Exception $exception) {
$this->logger->error(sprintf(
"ERROR: %s at %s line %s",
$exception->getMessage(),
$exception->getFile(),
$exception->getLine()
));
throw new CouldNotSendMessageException($exception->getMessage());
}
return true;
}
}
<?php
class FooMessage
{
private $channel_id;
private $message;
public function __construct(string $channel_id, string $message)
{
$this->channel_id = $channel_id;
$this->message = $message;
}
}
▼ 正常系テストの場合¶
メソッドのアノテーションで、@test
を宣言する。
*実装例*
リクエストにて、チャンネルとメッセージを送信した時に、レスポンスとしてTRUE
が返信されるかを検証する。
<?php
use FooMessage;
use FooNotifiation;
use PHPUnit\Framework\TestCase;
class FooNotificationTest extends TestCase
{
private $logger;
private $client;
public function setUp()
{
// 検証対象外のクラスはモックとする。
$this->client = \Phake::mock(Client::class);
$this->logger = \Phake::mock(LoggerInterface::class);
}
/**
* @test
*/
public function testSendMessage_FooMessage_ReturnTrue()
{
$fooNotification = new FooNotification(
$this->client,
"xxxxxxx",
$this->logger
);
$fooMessage = new FooMessage("test", "X-CHANNEL");
$this->assertTrue(
$fooNotification->sendMessage($fooMessage)
);
}
}
# Time: x seconds
# OK
▼ 異常系テストの場合¶
メソッドのアノテーションで、@test
と@expectedException
を宣言する。
*実装例*
リクエストにて、メッセージのみを送信しようとした時に、例外を発生させられるかを検証する。
<?php
use FooMessage;
use FooNotifiation;
use PHPUnit\Framework\TestCase;
class FooNotificationTest extends TestCase
{
private $logger;
private $client;
public function setUp()
{
// 検証対象外のクラスはモックとする。
$this->client = \Phake::mock(Client::class);
$this->logger = \Phake::mock(LoggerInterface::class);
}
/**
* @test
* @expectedException
*/
public function testSendMessage_EmptyMessage_ExceptionThrown()
{
$fooNotification = new FooNotification(
$this->client,
"xxxxxxx",
$this->logger
);
$fooMessage = new FooMessage("test", "");
$fooNotification->sendMessage($fooMessage);
}
}
# Time: x seconds
# OK
機能テストの場合¶
*実装例*
メソッドのアノテーションで、@test
を宣言する必要がある。
<?php
use PHPUnit\Framework\TestCase;
use GuzzleHttp\Client;
class FooControllerTest extends TestCase
{
/**
* @test
*/
public function testFoo()
{
// アプリケーション自身のControllerクラスにリクエストを送信する処理。
$client = new Client();
$response = $client->request(
"GET",
"https://example.com",
[
"query" => [
"id" => 1
]
]
);
// レスポンスの実際値と期待値の整合性を検証する。
}
}
▼ テストケース¶
外部Webサイトが正常であることを前提として、外部Webサービスとの連携が含まれていることに注意する。
HTTPメソッド | 分類 | 検証方法 | 期待値 (assert メソッド) |
---|---|---|---|
POST、PUT | 正常系 | リクエストのボディにて、必須パラメーターにデータを割り当てる。 | ・Controllerが200 ステータスを含むレスポンスを返信すること。・更新されたデータのIDが期待通りであること。 ・レスポンスされたデータが期待通りであること。 |
リクエストのボディにて、任意パラメーターにデータを割り当てない。 | ・Controllerが200 ステータスを含むレスポンスを返信すること。・更新されたデータのIDが期待通りであること。 ・レスポンスされたデータが期待通りであること。 |
||
リクエストのボディにて、空文字やnull が許可されたパラメーターに、データを割り当てない。 |
・Controllerが200 ステータスを含むレスポンスを返信すること。・更新されたデータのIDが期待通りであること。 ・レスポンスされたデータが期待通りであること。 |
||
異常系 | リクエストのボディにて、必須パラメーターにデータを割り当てない。 | ・Controllerが400 ステータスを含むレスポンスを返信すること。・レスポンスされたデータが期待通りであること。 |
|
リクエストのボディにて、空文字やnull が許可されたパラメーターに、空文字やnull を割り当てる。 |
・Controllerが400 ステータスを含むレスポンスを返信すること。・レスポンスされたデータが期待通りであること。 |
||
リクエストのボディにて、パラメーターのデータ型が誤っている。 | ・Controllerが400 ステータスを含むレスポンスを返信すること。・レスポンスされたデータが期待通りであること。 |
||
GET | 正常系 | リクエストにて、パラメーターにデータを割り当てる。 | Controllerが200 ステータスを含むレスポンスを返信すること。 |
異常系 | リクエストのボディにて、パラメーターに参照禁止のデータを割り当てる。 (認可の失敗) | Controllerが403 ステータスを含むレスポンスを返信すること。 |
|
DELETE | 正常系 | リクエストのボディにて、パラメーターにデータを割り当てる。 | ・Controllerが200 ステータスを含むレスポンスを返信すること。・削除されたデータのIDが期待通りであること。 ・レスポンスされたデータが期待通りであること。 |
異常系 | リクエストのボディにて、パラメーターに削除禁止のデータを割り当てる。 (認可の失敗) | ・Controllerが400 ステータスを含むレスポンスを返信すること。・レスポンスされたデータが期待通りであること。 |
|
認証/認可 | 正常系 | リクエストのヘッダーにて、認証されているトークンを割り当てる。 (認証の成功) | Controllerが200 ステータスを含むレスポンスを返信すること。 |
異常系 | リクエストのヘッダーにて、認証されていないトークンを割り当てる。 (認証の失敗) | Controllerが401 ステータスを含むレスポンスを返信すること。 |
|
リクエストのボディにて、パラメーターにリクエスト禁止のデータを割り当てる。 (認可の失敗) | Controllerが403 ステータスを含むレスポンスを返信すること。 |
▼ 正常系GET¶
Controllerが200
ステータスを含むレスポンスを返信することを検証する。
*実装例*
<?php
use GuzzleHttp\Client;
use PHPUnit\Framework\TestCase;
class FooControllerTest extends TestCase
{
/**
* @test
*/
public function testGetPage_GetRequest_Return200()
{
// 外部サービスがクライアントの場合はモックを使用する。
$client = new Client();
// GETリクエスト
$client->request(
"GET",
"/xxx/yyy/"
);
$response = $client->getResponse();
// 200ステータスが返却されるかを検証する。
$this->assertSame(200, $response->getStatusCode());
}
}
▼ 正常系POST¶
Controllerが200
ステータスを含むレスポンスを返信すること、更新されたデータのIDが期待通りであること、レスポンスされたデータが期待通りであることを検証する。
*実装例*
<?php
use GuzzleHttp\Client;
use PHPUnit\Framework\TestCase;
class FooControllerTest extends TestCase
{
/**
* @test
*/
public function testPostMessage_GetRequest_Return200NormalMessage()
{
$client = new Client();
// APIにPOSTリクエスト
$client->request(
"POST",
"/xxx/yyy/",
[
"id" => 1,
"message" => "Hello World!"
],
[
"HTTP_X_API_Token" => "Bearer <APIキー>"
]
);
$response = $client->getResponse();
// 200ステータスが返却されるかを検証する。
$this->assertSame(200, $response->getStatusCode());
// レスポンスデータを抽出する。
$actual = json_decode($response->getContent(), true);
// 更新されたデータのIDが正しいかを検証する。
$this->assertSame(1, $actual["id"]);
// レスポンスされたメッセージが正しいかを検証する。
$this->assertSame(
[
"データを変更しました。"
],
$actual["message"]
);
}
}
▼ 異常系POST¶
Controllerが400
ステータスを含むレスポンスを返信すること、レスポンスされたデータが期待通りであること、を検証する。
*実装例*
<?php
use GuzzleHttp\Client;
use PHPUnit\Framework\TestCase;
class FooControllerTest extends TestCase
{
/**
* @test
*/
public function testPostMessage_EmptyMessage_Return400ErrorMessage()
{
$client = new Client();
// APIにPOSTリクエスト
$client->request(
"POST",
"/xxx/yyy/",
[
"id" => 1,
"message" => ""
],
[
"HTTP_X_API_Token" => "Bearer <APIキー>"
]
);
$response = $client->getResponse();
// 400ステータスが返却されるかを検証する。
$this->assertSame(400, $response->getStatusCode());
// レスポンスデータのエラーを抽出する。
$actual = json_decode($response->getContent(), true);
// レスポンスされたエラーメッセージが正しいかを検証する。
$this->assertSame(
[
"IDは必ず入力してください。",
"メッセージは必ず入力してください。"
],
$actual["errors"]
);
}
}