api-first-specにBadRequestを一気にテストする機能を追加したので、その紹介です。
api-first-specって?
WebAPIの定義をjsで記述して、それをそのままテストにも使ってしまおうという試みです。詳細はこちらに書きました。
BadRequestとは?
WebAPIに対する不正なリクエストメッセージのことです。
例えば、APIの仕様としてユーザ名は6文字以上、32文字以下と規定してあったとします。
この場合、5文字以下や33文字以上のユーザ名を送りつけてくるリクエストはBadRequestです。
仕様としてそれはやるんじゃない!と明記されている事項に違反しているのでBadRequestと呼ばれます。
BadRequestに対するサーバ側の対処方法はいろいろなやり方がありますが、一番シンプルで正当な方法はHTTPレスポンスのステータスコードとして400を返すことです。
400はHTTPの仕様でBadRequestに対するステータスコードとして定められており、このステータスコード自体もBadRequestと呼ばれます。(404をNotFoundと言うのと同じノリです。)
ちなみにBadRequestレスポンスではBody等は空でも良く、詳細なエラー理由等は返さなくても構いません。
もちろん、原因が推測できるメッセージを付けた方が親切で、そういう考え方もアリなんですが、一方でBadRequestはクライアント側で適切にエラーチェックが行われていれば発生しないものなので、
- BadRequestが発生する
- クライアントのバグ ー> 直せば良い
- それ以外の要因??? ー> 攻撃の可能性が高い
と考えることもできて、この立場からは攻撃者に余計な情報を与えないためにエラーメッセージは無い方が良いという考え方もあります。
個人的にはGitHub APIのように広く一般に使ってもらうことを目的としたAPIではBadRequestにもエラーメッセージを付けた方が良いけど、サーバもクライアントもすべて身内で作る一般のWebサービスではつける必要は無いと思っています。
どうやってテストするの?
現物を見た方が早いので、現在開発中のシステムで作っているユーザsignupのspecをGistに公開します。
細かい説明は省略しますが、詳細を知らなくてもなんとなく読めると思います。
リクエスト定義
Specのリクエスト定義部分は下記のようになっています。
"request": {
"contentType": spec.ContentType.URLENCODED,
"params": {
"name": "string",
"email": "string",
"password": "string"
},
"rules": {
"name": {
"maxlength": 255,
"pattern": "^[0-9a-zA-Z_-]*$",
"required": true
},
"email": {
"email": true,
"maxlength": 255,
"required": true
},
"password": {
"minlength": 6,
"maxlength": 255,
"pattern": "^[\x21-\x7F]*$",
"required": true
}
}
},
この定義では以下のことが仕様として規定されています。
- signupのパラメータはname, email, passwordの3つ
- データ型はすべてstring
- nameは
- 最大255文字
- 仕様できる文字は英数字と「-」「_」
- 必須
- emaiは
- メールアドレス形式
- 最大255文字
- 必須
- passwordは
- 最小6文字
- 最大255文字
- 仕様できる文字は制御文字を除くASCII
- 必須
見たまんまっすな。(^^;
さて、この仕様からBadRequestを洗い出すと以下のケースがあることがわかります。(仕様をひっくり返しただけなので別に読まなくても良いです。(^^;;;)
- nameが256文字以上
- name英数字「-」「_」以外を使っている
- nameがない
- emailがメールアドレス形式で無い
- emailが256文字以上
- emailがない
- passwordが5文字以下
- passwordが256文字以上、
- passwordに制御文字かASCII以外の文字が使われている
- passwordがない
ruleの数だけ対応するBadRequestがあります。
また、今回はパラメータがすべてstringなのでありませんが、データ型がintのフィールドに対して文字列を投げる、等もBadRequestです。
真面目にテストを書こうとするとこれらすべてのケースに対して、テストケースを作らなければならないわけです。。。。やっとれんわ!!!
たった3つのパラメータしかない1つのAPIで、これです。
システム全体をテスト駆動で開発することに絶望するには十分すぎるコストですな。(--
まとめてBadRequestをテスト
している部分がここです。
host.api(API).params({
"name": "user2",
"email": "user2@test.com",
"password": "password"
}).badRequestAll({
"name": ["123@", "あいうえお"],
"email": ["dot..dot@test.com", "sample@test"],
"password": ["abc def", "あいうえおかきくけこ"],
});
このコードが何をしているかというと。。。
- 最初にエラーとならない正当なパラメータセットを定義する
- ruleをイテレートして正当なパラメータセットから1個づつ不正なパラメータに置き換えてリクエストを投げまくる
- requiredならその項目を削る
- maxlengthならmaxlength +1の長さの文字列を生成する
- emailはメールアドレスではない単純文字列を使用する
- ただしpatternはアンマッチな文字列を自動生成できないので自動生成テストの対象外
- その替わりbadRequestAllの引数に渡されたパラメータセット(配列も可)をイテレートして、一つづつパラメータを置き換えたリクエストを投げることができる
という感じです。
内部的にはmochaを使用しているので、一つづつitコールを生成してそのリクエストがBadRequestになることを検証しています。
結果の出力はこんな感じです。
BadRequest test for
✓ name - maxlength(1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456)
✓ name - required(undefined)
✓ name - 123@
✓ name - あいうえお
✓ email - email(invalidemail)
✓ email - maxlength(3456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456@example.com)
✓ email - required(undefined)
✓ email - dot..dot@test.com
✓ email - sample@test
✓ password - minlength(12345)
✓ password - maxlength(1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456)
✓ password - required(undefined)
✓ password - abc def
✓ password - あいうえおかきくけこ
おーるぐりーん(^^v
BadRequestの判定方法を変更する
こっちに書いてあります。
リクエスト/レスポンスの詳細を見る
デフォルトではテスト結果にはどのテストが失敗し、その原因はなんであるかの簡単なエラーメッセージしか表示されません。
でも、エラー時には実際にどんなリクエストを投げてどんなレスポンスが返ってきたのかを知りたいですよね。
そういう場合は実行時に環境変数を指定することで全通信の詳細を見ることができます。
env API_FIRST_SPEC_VERBOSE=true mocha test/signup.spec.js
制限事項
例えば
- patternでアルファベットのみ許可
- maxlength 10文字
というruleがあった場合、現行仕様ではmaxlengthの違反文字列として「12345678901」という文字列を生成します。
この文字列はpatternにも違反しているので、サーバからBadRequestレスポンスが返ってきたとしても、どちらのルール違反によるものかを判断することができません。
実際にはサーバ側ではpatternだけがチェックされていて、maxlengthのチェックは実装し忘れているかもしれないわけです。
自動生成では、このように生成した文字列が複数のルールに違反するケースというのがどうやっても残りえると思っていますが、そのようなテストケースはほとんど無価値です。
この場合はbadRequestAllの引数で単一ルール違反となるような文字列を組み立てて自力でテストケースを追加してください。
まとめ
実際のところ、これはまだ昨日作って今日ちょっとテストした、という段階のシロモノなんですが、無茶苦茶便利なんじゃないかという気がしています。
賭けてもいいですが、僕が過去に作ったものは個のテストを全部はパスしません。(爆)