アノテーションとAPIレスポンスをアサーションできるライブラリ作った

概要

アノテーションAPI レスポンスをアサーションできるようにするPHPのライブラリを作った。

アノテーションは、swagger のフォーマットを基準にしている。 swagger の説明は別に書いた ので、必要であれば参照してほしい。

インストール

composer からインストールする

    "require-dev": {
        "gong023/swagger-assert": "dev-master"
    }

なお、PHP5.4 以上必須。

実例

以下の様な API があり、そこに swagger のフォーマットでアノテーションが書かれていたとする。

swagger-assert を使うと、/plain のレスポンスが、swagger の記述どおり idname キーを持っているかをアサーションできる。

<?php
**
 * @SWG\Resource(
 *     resourcePath="plain",
 *     @SWG\Api(
 *         path="/plain",
 *         description="plain api structure",
 *         @SWG\Operation(
 *             method="GET",type="SimpleMember",nickname="plain"
 *         )
 *     )
 * )
 *
 * @SWG\Model(
 *     id="SimpleMember",
 *     @SWG\Property(name="id", type="integer", required=true, description="user id"),
 *     @SWG\Property(name="name", type="string", required=true, description="user name")
 * )
 */
$app->get('/plain', function () use ($app) {
    $response = [
        'id'   => 0,
        'name' => 'kohsaka'
    ];

    return $app->json($response);
});

使うには、まずテスト開始時に、SwaggerAssert::analyze を呼ぶ。引数には、アノテーションを書いたファイルがあるディレクトリパスを渡す。

<?php
// testing bootstrap.php
\SwaggerAssert\SwaggerAssert::analyze($targetDir);

続いて、テストクラスで SwaggerAssert::responseHasSwaggerKeys を呼ぶ。引数に必要な情報は以下である。

  • 第一引数:API レスポンスの配列
  • 第二引数:リクエストの HTTP メソッド名
  • 第三引数:リクエストのエンドポイントの URL 名

PHPUnit を使って先述の API をテストする場合、以下の様なコードになる。

<?php
class PlainApiTest extends \PHPUnit_Framework_TestCase
{
    public function testResponseHasSwaggerKeys()
    {
        $response = $this->request('get', '/plain');
        $result = \SwaggerAssert::responseHasSwaggerKeys(array $response, 'get', '/plain', $onlyRequired = true);

        $this->assertTrue($result);
    }
}

\SwaggerAssert::responseHasSwaggerKeysAPI レスポンスのキーと swagger に記述されたキーを比較し、一致した場合は true を返す。一致しない場合は以下のようにエラーメッセージを出力する。

SwaggerAssert\Exception\CompareException: Failed asserting that API response and swagger document are equal.
--- Response
+++ Swagger
@@ @@
 Array (
-    0 => 'id'
-    1 => 'name'
+    0 => 'name'
 )

なお、引数の四つ目はオプションである。 false を渡すと swagger で required=false になっている必須レスポンスでないキーもアサーションの対象にする。デフォルト値は true になっている。

動機

開発に swagger を導入しているプロジェクトがある。 swagger の導入により API の仕様を記述するための統一されたフォーマットが提供され、またアノテーションという形でコードに近い場所で仕様が読めるようになった。これらのおかげでドキュメント事情は wiki で管理するよりも良くなった。

一方で、swagger の内容と実際の API レスポンスが一致しないというトラブルもよく起きた。食い違いがあると、ドキュメントは混乱の種となり逆に開発を阻害してしまう場合すらある。

これを解消するため、API結合テスト時、レスポンスが swagger の内容と一致しているかどうかをアサーションできるライブラリを作成した。

雑感

すごく雑に思ったことを書く。

swagger-assert と autodoc

autodoc というグレートな gem があり、これはテスト中のリクエスト及びレスポンスをそのまま markdown に吐き出してくれる。 このアプローチを取る限り、ドキュメントとレスポンスに食い違いが生じることはない。 つまり単純に「ドキュメントとレスポンスが食い違う」という問題を解決したいのならば、autodoc のようなアプローチをとるべきであり、swagger-assert のアプローチは非常に筋が悪い。

一方で、swagger に厳しいほど色々なパラメータが用意されているお陰で、アサーションを楽に行えるケースもある。 例えば autodoc のようなアプローチだとレスポンスパラメータの型を知ることはかなり困難だが、swagger からなら簡単に導き出せる。 また、ご存知の通り卓球ハウスには id:r7kamura がいるので色々聞いてみたところ、テストクラスに決まったフォーマットを要求するため既存のプロジェクトに導入への面倒くさい、とかいくつか欠点もあるらしい。 そういった意味ではどこにでもアノテーションさえあれば良い swagger-assert の方が導入は楽ではある。

あとはそもそもドキュメントとして swagger の方が圧倒的に情報量が多いとかそういうメリットもあるが、代わりにアノテーションの仕様について覚える手間もある。 総じて、低いコストで十分なドキュメントを得られる autodoc と、学習コストは高いが詳細なドキュメントを得られる swagger という傾向はあると思う。 まぁ、gem と composer 比べてどうすんだという話はあるが、アプローチの違いが面白いのでまた色々考えてみたい。

PHP

自分は世間で言われているほど PHP が悪い言語だと思っていなくて、むしろゆるく Java っぽく書けるところは割と好きだったりする。 (そもそも Java がそんな嫌いじゃなくて、ああいう言語でドメインオブジェクトが先鋭化されていく時に感じる万能感は ruby が謳う万能感とはまた違う気持ちよさがあると思う。)

ただ、PHP の array はどうしても頂けない。PHPの array はかなりフリーダムな仕様で、collection も勝手に hash にしてしまう。例えば、array(‘a', ‘b', ‘c’) は array(0 => ‘a’, 1 => ‘b’, 2 => ‘c’) にしてしまう。 当然 array の中の型を一律に保証することもできない。string も integer もユーザー定義のクラスも一緒くたに array に混ぜることができる。もっと言えば key にくる型も特に保証できない。 また、全てがオブジェクトという言語でもない。そのせいか知らないが、PHPはとにかくなんでも array にぶち込んで返す習慣が横行してしまっている。少し気を抜くと返ってくるのは大抵このフリーダム array になる。 このため、ことPHPだとプログラミングの労力の多くをこのフリーダム array との闘いに費やさなければならない。 そんな状況を知ってか知らずか、PHPには array 操作のための組み込み関数が異様に多い。

ただ、それらは array を丁寧にクラスにしてあげれば大抵解決できる話なので、なるべく組み込み関数を使わないようにすることを心がけた。 その甲斐あって、30クラス1387行のライブラリで array 系組み込み関数は9回しかでてこない(適当な grep なので間違っているかも)。ちなみに組み込み関数自体の出現は24回。

grep している間に何が言いたいのかよくわからなくなってきたが、要は The ThoughtWorks Anthology にオブジェクト指向強制ギプスというのがあるが、 PHPに限った話で言えば組み込み関数を使わないでプログラミングするとそれっぽい効果が得られるかも、みたいなことが言いたかった。強制ギプスほどキツい制約ではないのでヌルくやるにはオススメかもしれない。