LoginSignup
7
2

More than 3 years have passed since last update.

【WIP】PHPUnitで私好みのアサーションを足して「読みやすい書きやすい」テストを

Last updated at Posted at 2017-01-20

@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メソッドを実装しておけば :ok: ぽいです。

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, '存在しないはずのページの状態を検知できていない');
7
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2