@todo ちゃんと動作検証
@todo 各クラスの詳細・役割分担について書く
現在、業務でAPIアプリケーションを開発しています。
APIということで、「エラーの形式が決まって」います。
その時に、 _viewに入っている変数をとってその中の $codeがXXX
$sub_codeがYYY
$messageがZZZ
みたいなアサーションを、毎回書く?3つも?それって・・・
あまり穏やかじゃないね、と感じた次第です。
※実際にはもう少し複雑。端折っていますが、大体こんなイメージです。
「同じような処理なら」「同じ書き方を」「しかも、なるべく簡潔・一目瞭然に」やるのが美しくないですか?
ということで、「エラー時のレスポンスのアサーション」を丸っとまとめてしまえば楽じゃないですか!!と思ったわけです。
作りたいモノ
例えば、TwitterのREST APIのレスポンスのイメージで、その内容を検査するモノ!を作ってみましょう。という想定でススメます。
{
"errors": [
{"message":"Sorry, that page does not exist","code":34},
{"message":"User not found.","code":50},
{"message":"Sorry, you are not authorized to see this status","code": 179}
]
}
※あくまで「例」ですよ!実際にはコレらのエラーが、まして同時に複数個返ってくるっけ?とか突っ込むのは野暮です。
cf: Error Codes & Responses — Twitter Developers
コレに対して、
$this->assertErrorCode(34, $errors, '存在しないはずのページの状態を検知できていない');
$this->assertErrorCode(50, $errors, '存在しないはずのユーザーの状態を検知できていない');
$this->assertErrorCode(179, $errors, 'アクセス資格のないリソースにアクセスできてしまっている');
とかやれると = レスポンスをいちいちデコードしてkey/valueチェックをして・・って書くよりシュッとするので「テストしたい内容を、コードに明瞭に落とし込める」です。
実装概要(PHPUnitのAPI)
公式docは以下にあります。しっかり言及されていますね
-> カスタムアサーションの作成
なるほど、どうやら Constraint
matcher
及び TestCase のサブクラス
というのがキーワードとなりそうですね。
・・とはいえ、個人的には「ちょっと駆け足気味」あるいは「ややオラついた」記述では・・・という印象を受けました。
具体例が欲しいですね。生コードを見てみましょう。
-
Constraint
- アサーションのロジックを、それぞれ独立したクラスとして定義していきます
-
assertXxx()
の実体- これが役割的に「TestCaseのサブクラス」の代わりをしているイメージになります。
オマケ:
私はCakePHPを普段使っているものですから、Cakeでの実装例も併せて紹介しておきます
実装をしてみよう
インターフェイスとロジック
どんなものがあれば良いでしょう?
「検査対象(json文字列)」に対して「code
message
のそれぞれのキーが存在し」「かつ、パラメータとしてアサーションに渡された内容と合致する」ことを保証する、みたいな感じで如何でしょう。
愚直に書き下すならこんな感じに。
$data = json_decode($json);
if (! $json) {
return false;
}
if (! (property_exists($json, 'code') && property_exists($json, 'message'))) {
return false;
}
return ($json->code === $expected_code, && $json->message === $expected_message);
これを「ロジック」として、アサーションメソッドのロジックはconstraintに組み込むというお作法に従って、クラスを定義してみます。
具体的には、 (__construct()
と)matches()``toString()
というpublicメソッドを実装しておけば ぽいです。
class ApiResponse extends PHPUnit_Framework_Constraint
{
// 検査対象となる内容を格納する
private $data;
// expectedとの比較を行う前のフェーズで「失敗」させる事も可能
public function __constuct(string $json)
{
$data = json_decode($json);
if (! $data) {
// FailedErrorを投げることで、任意のタイミングでアサーションを失敗させられる
throw new PHPUnit_Framework_AssertionFailedError('Failed to parse json.');
}
if (! (is_object($json) && property_exists($json, 'code') && property_exists($json, 'message')) {
return false;
}
$this->data = $data;
}
public function matches($expected)
{
// 失敗ならfalse、成功ならtrueを返すだけでOK
return (
$this->data->code === $expected['code']) &&
$this->data->message = $expected['message']
);
}
public toString()
{
return sprintf('%s has not valid code and message.', json_encode($this->data));
}
}
ロジックはできました。あとは「呼び出し元」です。
これが「TestCaseのサブクラス」という事になります。
class AssertApiResponse extends PHPUnit_Framework_TestCase
{
public function assertApiResponse($code, $messsage, $content, $message = '')
{
$this->assertThat(compact('code', 'message'), new ApiResponse($content), $message);
}
}
あとは、こんな形で取得したレスポンスに対するアサーションが可能になります。
$error = $response['errors'][0];
$this->assertApiResponse(34, 'Sorry, that page does not exist', $error, '存在しないはずのページの状態を検知できていない');