自分が考える最強のテストレポートを求めて

テストレポートは重要である。 それはフィードバックを正確に得るため。 正確なフィードバックは開発を加速させる。そして新たな気付きを得ることができる。

必要な機能、自分がほしい機能をまとめることで、最強のテストレポートを考察してみたい。

現状のテストレポートから

他のテストフレームワーク、テストレポートツール、他の人の発表資料を見ればそれらが見えてくる。

JUnit5

JUnit5はTestExecutionListenerをImplementしているクラスでテストレポートを作成できる。

通常、コンソールでテストレポートを表示するConsole Launcherは以下のようになる。

├─ JUnit Vintage
│  └─ example.JUnit4Tests
│     └─ standardJUnit4Test ✔
└─ JUnit Jupiter
   ├─ StandardTests
   │  ├─ succeedingTest() ✔
   │  └─ skippedTest() ↷ for demonstration purposes
   └─ A special test case
      ├─ Custom test name containing spaces ✔
      ├─ ╯°□°)╯ ✔
      └─ 😱 ✔

Test run finished after 64 ms
[         5 containers found      ]
[         0 containers skipped    ]
[         5 containers started    ]
[         0 containers aborted    ]
[         5 containers successful ]
[         0 containers failed     ]
[         6 tests found           ]
[         1 tests skipped         ]
[         5 tests started         ]
[         0 tests aborted         ]
[         5 tests successful      ]
[         0 tests failed          ]

取得できる情報は以下。

  • テスト名
  • テストステータス
  • グルーピング
  • 総実行時間
  • ステータスごとのテスト総数

Jest

以下、成功時の例。

 PASS  ./sum.test.js
  ✓ adds 1 + 2 to equal 3 (3ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.033s
Ran all test suites.

以下、失敗時の例。エラー時に対象のコードと詳細なエラー内容が出力されるのはよい。

 FAIL  ./sum.test.js
  ✕ adds 1 + 2 to equal 3 (5ms)

  ● adds 1 + 2 to equal 3

    expect(received).toBe(expected) // Object.is equality

    Expected: 4
    Received: 3

      4 |
      5 | test('adds 1 + 2 to equal 3', () => {
    > 6 |   expect(sum(1, 2)).toBe(4);
        |                     ^
      7 | });
      8 |

      at Object.<anonymous> (sum.test.js:6:21)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        1.385s
Ran all test suites.

ここには表示できてないが、実行中は何のテストをしているかとその経過時間が表示される。

Jestで取得できる情報は以下。

  • テストファイル名
  • テストケース名
  • テストステータス
  • 失敗した場所のコードとそのログ
  • グルーピング
  • 総実行時間
  • ステータスごとのテスト総数
  • (実行中)実行しているテストとその経過時間

Allure

取得できる情報は以下。テスト側に設定が必要な項目もある。非常に情報量は多い。

  • テストのステータス
  • テストのステータス比率
  • 実行環境
  • Suite
  • テスト数とそのステータスの推移
  • 環境情報
  • Category
  • 実行時間の推移
  • Severity
  • タイムライン
  • Behavior
  • Package
  • チケットリンク
  • テスト管理リンク
  • 添付(スクリーンショットや動画など)
  • エラーログ

f:id:AHA_oretama:20191122123825p:plain:w200 f:id:AHA_oretama:20191122123931p:plain:w200 f:id:AHA_oretama:20191122124004p:plain:w200 f:id:AHA_oretama:20191122124026p:plain:w200 f:id:AHA_oretama:20191122124045p:plain:w200 f:id:AHA_oretama:20191122124104p:plain:w200 f:id:AHA_oretama:20191122124121p:plain:w200 f:id:AHA_oretama:20191122124147p:plain:w200

ReportPortal.io

Allureと比較して目新しい情報はない。 テストレポート+ワークフロー(チケット管理とのインテグレーション、テストのパイプライン)の機能に価値がありそう。 唯一、他と異なるのはAIによりカテゴリ分けをしてくれること(過去のカテゴリと比較していると思われる)。

https://reportportal.io/features

UIテストレポートに関する発表資料

UIテストのレポータにフォーカスしているが、全体としてテストレポートに必要な機能がまとめられていてわかりやすい。

speakerdeck.com

ここまでのまとめ

  • ベースとなる機能(ログやテストステータス、実行時間)はほぼ共通で持っている。
  • レポートとしてフォーカスしているツールは、ダッシュボードやメトリクスなど可視性が高くなっている。

テストレポートの可能性

現状のテストレポートにはない機能だとしても有用な機能はあるはずである。 テストレポートの新しい機能の可能性について考えてみる。

新旧比較して過去のテストとの一致/類似度を表示させる

同じ原因のエラーが何度も表示される。 これはE2Eテストのように自身の関与が及ばない領域までテスト範囲に及んでいる場合に多いと思われる。

そのときに

  • 新規の問題なのか?それとも既知の問題なのか?
  • 既知の問題であれば、どのような問題と一致しているか?もしくは完全に一致しなくても類似度はどれぐらいか?
  • 過去に発生したのはいつか?何度発生しているか?

の情報が表示されると問題の解決が楽になる。

SLO の観点としてのテストレポート

テストは落ちたらすぐに治すべきものであり、それは絶対である。

しかしながら、そうできないものもあるもの事実である。 例えば、10回のうち1回ぐらい失敗するが、再現性が不明で原因がつかめないもの。 みなさんも経験があるのではないだろうか。

そのような場合に、SLOの観点を導入してみてはどうだろうか? 1テストケースに対する信頼性、テストケース全体での信頼性。 その信頼性を下回れば、他の作業を止めてでも問題解消につとめなければならない。 (もしくは該当のテストケースを削除する)

これを満たせなければ価値(後述)がないと判断できる。

価値としてのテスト

テストは資産である。資産であるが、それが正の資産か、負の資産(負債)であるかは判断がつかない。 またその資産を増やすか切り捨てる(テストケースを増やす、削除する)判断をするときに根拠が欲しくなる。

テストレポートで価値を定義してはどうか? 価値となりうるものは

  • テストケース数(コード数に対する)
  • カバレッジ
  • ドキュメント性
  • 実行回数
  • 信頼性
  • 実行時間

などが考えられる。

自分が考える最強のテストレポート

自分が考える最強のテストレポートには、テストレポートのベースとなる機能はMUSTであってほしい。

  • ログ
  • テストステータス
  • テスト時間
  • 全体のテスト結果
  • フィルタ、ソート
  • カバレッジ
  • スクリーンショット、動画(テストサイズSmall以外(後述))
  • 周辺環境のログ(テストサイズSmall以外(後述))

etc.

そして、それらにプラスしてテストレポートにすることの価値が上がる機能を全て盛り込んであるレポートであってほしい。

  • メトリクス(ダッシュボード)
  • SLO
  • 価値
  • 一致/類似度

etc.

そのようなテストレポートツールは残念ながら今のところ見当たらない。 時間があれば、自分が考えた最強のテストレポートを自分の手で作ってみたいところだ。

補足: UI テストレポート とUnit テストレポートでの違い

そこまで大きな違いはない認識。 ただし、UI テストでは連携している部分が多いため、その分必要なものが多くなる。

UI テスト とUnit テストでの違いというよりはGoogleのテストサイズによる違いといったほうが正確。

testing.googleblog.com

GoogleのテストサイズSmall以外(とくにLarge)では、インテグレーションする項目が増えるため、 それらに関する情報もテストレポートにあったほうがよい。 例えば、データベースのログやUIのスクリーンショットや動画など。

TypoFixer というサービスを作ってみて感じたこと

初めに

こんにちは、 おれたま@AHA_oretamaです。 Software Engineer in Test(SET)として活動しているエンジニアです。

今回、GitHubのプルリクエストで、タイポ(打ち間違え)を指摘して自動で修正するアプリを作りました。 作ってみて多くのことがわかり、そして自分の想定の甘さと自分の経験のなさを痛感し、もっと開発だけではなく全てのことを改善していかなければならない、と強く感じました。 今回はそのことを書いていきたいと思います。

今回はほぼ技術的な話はありません。

TypoFixer

まずは作成したアプリを紹介します。TypoFixerです。

GitHub Appsとして作られているため、以下のような特徴を持ちます。

  • Repository単位で設定ができる。
  • 有償・無償の設定ができる。
  • GitHub Appsとしてインストールすればすぐに使うことができる。

現段階ではとくにオプションなどもなく、カスタマイズできないため、インストールすればそのまま使用できます。

TypoFixerは以下のような機能を持ちます。

  1. プルリクエストを作成したときに、タイポがあればTypoFixerがレビュコメントを追加する。
  2. そのコメントにはタイポの修正候補も表示されており、その中から適切なものを選択すれば、TypoFixerが自動でタイポを修正してコミットしてくれる。
  3. TypoFixerに指摘された単語がタイポではない場合、「Not Typo」を選択すれば、辞書に登録され、TypoFixerは以後、同じ指摘をしなくなる。

背景

このサービスと作る前に、同じようなサービスを探してみるとTypotが見つかりましたが、いくつかの点で自分の中で満足がいくとは言えませんでした。

  • (造語やサービスなどの固有名詞など)実はタイポではないがタイポとして指摘された単語は、その後も指摘され続けてしまう。
  • Typotで使用しているスペルチェックライブラリpyenchantがRepository上でメンテナンスしないことを公言している。
  • 上記の対処もなく、コミットログが止まっている点などからTypotというサービスがメンテされ続けているとは言いにくい状態である。

また、上記とは別にモチベーションの部分として、GitHub Appsを自分で作成した経験を得て、GitHubのプルリクエストやIssueなどに対して、何かしらの操作(ここではプルリクエストへのレビュ)ができるようになることは、今後、SETの活動を進めていく上で役に立つと思い、自分の勉強のために始めました。

そして、もしこのツールが普及したときに、運用・維持費(できれば儲けまで)を得ることができればよいという淡い期待もありました。

やってみた良かった点

以下にやってみてよかった点をあげますが、やっぱり自分の手で実装したことによって得られたものだと思いますので、手を動かして実装することはよいことだと思いました。

  • 自身で足りないと思っていた機能は実装でき、(当初)自分がほしかったサービスができた
  • Circle CI 2.0, Herokuなど今まで仕事などでは、あまり使ったことがないツールをしっかりドキュメント読みこんで、学びながら動かす機会ができた。

作ってみたあとに気づいた点

これらは実際に自分で作ってみて、試したあとに気づいた点です。

  • 実際に使ってみようとしたところ、自分が想定していたものとは以下の点で異なり、非常に使いにくかった
    • タイポの指摘の件数が多すぎる
    • タイポを自動修正したときに整合性が取れない(例えば、メソッド名を変更した場合にそれを呼び出している箇所までは修正しない)ことがある
    • 指摘する箇所に合わせて指摘内容を変更してほしい(例えば、ライブラリのimportする部分などは指摘しないでほしい)。

これらのことは作る前にプロトタイプを作ったり、サービスの動きを想定しなりなどをもっと事前に準備をしていれば、避けられた点かと思います。

また、現段階では、到底お金をもらうサービスには成り得てません。 このようにサービスを簡単に作ることは難しいことがわかりました…

今後の予定

反省点として、やはり自分がどういうものを作りたいか、そして実際にどのような動きになるかをプロトタイプなどを作って前段階で検証する、 つまり、要件定義の段階をもっと自分で行わなければならないと感じました。 そしてその力が自分には足りていないと実感することもできました。

あまりの使いにくさに一度このままサービスをクローズさせようかとも思いましたが、 とはいえ、このまま終わらせようとは思っていません。

せっかく作ろうと思ったサービスなので、もう少し形になるまで続けようとは思います。 そして、できればこのサービス開発で、自分なりの何かをつかめればよいと思っています。

これからやろうと思っている改善点

いまいまやろうと考えている改善点です。まだまだ構想段階です。

  • ファイル拡張子からタイポの指摘箇所を調整する。例えば、マークダウンなどのドキュメントであれば全てを対象として指摘する
  • 設定をカスタマイズ可能して、そのRepositoryにあった指摘を行うことができるようにする
  • 言語ごとの構文を把握して、構文にあった指摘に最適化する

Monitoring 〜雑まとめだけどドコかに残しておきたい〜

Monitoringについて詳しくないので、Web Developer Roadmap 2018が出たので2017年版と比較してみるMonitoring and Alertingにあるツールを全て調べてみた。

あとちょうど最近、流行っていると聞いたDataDog、Grafanaについても調べてみる。

Zabbix

インフラ寄りのイメージ。
ネットワーク、サーバ、クラウド、サービスと監視対象範囲は非常に広い気がする。

https://www.zabbix.com/jp/java_applications
JavaアプリケーションはJMXによる監視のみ。
ヒープ、メモリなどのJVM寄りの監視。

インフラ監視には◯、アプリケーション監視には✕。

Prometheus

https://prometheus.io/docs/instrumenting/clientlibs/
https://github.com/prometheus/client_java
完全にアプリケーション監視向け。

アプリケーション側にログを埋め込むため、アプリケーション側に改修が必要になるが、
その分、柔軟に必要な情報(例えばメソッド単位でのパフォーマンスなど)を取得することができる。

Graphite

Graphite自体は2つのことしかやらない。

  • Store numeric time-series data
  • Render graphs of this data on demand

なので、データ収集は他ツールとつなげる必要がある。

ツールはこちら。
http://graphite.readthedocs.io/en/latest/tools.html

オールインワンではないので、カスタマイズは柔軟だが構築に手こずりそう。

Munin

どうでもいいけどとにかくトップページがダサい。http通信だし…

Munin is a networked resource monitoring tool that can help analyze resource trends and "what just happened to kill our performance?" problems. It is designed to be very plug and play. A default installation provides a lot of graphs with almost no work.

プラグイン形式。
http://munin-monitoring.org/wiki/plugins

JavaJMXの監視。
https://github.com/munin-monitoring/contrib/tree/master/plugins/jmx

PagerDuty

http://blog.serverworks.co.jp/tech/2015/10/13/start-pagerduty/ (こっちのほうがわかりやすい)

つまり、通知サービス。
監視は別で、監視からの集約を担う。
集約した情報からSlack,SNS,メールへのエスカレーションを行う。

New Relic

いくつかのサービスに分かれている。
https://newrelic.com/products
アプリ、WEB、WEBアプリ、アプリケーション、インフラとダッシュボードのInsightsに分かれており、
広範囲のモニタリングがNew Relic製品の組み合わせで可能になる。

Javaについてもインストールは簡単で、エラー、トランザクション、レスポンスタイム、外部システムのレスポンスタイムなど必要な情報は見ることができる。
https://newrelic.com/java

Nagios

プラグイン形式。豊富なプラグイン
サーバ、ネットワーク、アプリケーションをカバー。

https://www.nagios.com/solutions/java-monitoring/
アプリケーション監視自体はJavaを見る限り、アプリケーション内部ではなく、メモリとかがメイン。

AppDynamics

アプリケーションのパフォーマンス、モバイル、ブラウザなどアプリケーション層を幅広くサポート。
またデータベースやサーバもサポートしている。

Java,Springにも対応している。
https://www.appdynamics.jp/java/
https://www.appdynamics.jp/java/spring/

DataDog

可視化がメインのよう。UIがすごいリッチ!

非常に豊富なインテグレーションをもつ。
https://www.datadoghq.com/product/integrations/#all

JMXによるJava監視が可視化。
https://docs.datadoghq.com/integrations/java/

Grafana

ビジュアライズツール。Kibanaみたいなツールと認識。

WEB API The Good Parts 6章の自分的まとめ

今回で本シリーズは最後となる。
以下の本の6章を自分的にまとめる。

Web API: The Good Parts

Web API: The Good Parts

もしAPI を開発している、もしくはAPI を日々触っているのであれば、 是非買って読んでみてほしい。

堅牢なWeb API を作る

今回は重要なポイントは多いが、疑問や問題点はなかったので、 各章を一言でまとめて終わろうと思う。

  • どんなセキュリティの問題があるか
    以下の3つのパターンに分けて考える。
    • サーバとクライアントの間での情報の不正入手
    • サーバの脆弱性による情報の不正入手や改ざん
    • ブラウザからのアクセスを想定しているAPI における問題

サーバとクライアントの間での情報の不正入手

  • HTTPS にする。ただし100%安全であるとは言えないが、有効な手段である。

サーバの脆弱性による情報の不正入手や改ざん

  • XSS対策
    JSON をブラウザが必ずJSON と認識するようにする

    • Content-Type: application/json を指定する
    • IE8以上対策 → X-Content-Type-Optioins: nosniff を指定する
    • IE7以下対策 → 追加のリクエストヘッダをつけてそれを検証する or JSON をエスケープする

  • XSRF対策

    • データの更新にはPOST,PUT,PATCH,DELETE を用いる
    • XSRF トークンを使う。(そのサイトが発行したワンタイムトークン、もしくはセッションごとにユニークなトークン)

  • JSON ハイジャック
    JSON ハイジャックを防止するには、現在のところ以下の対策が有効。

    • JSON をSCRIPT 要素では読み込めないようにする
      → 追加のリクエストヘッダをつけて検証する

    • JSON をブラウザが必ずJSON と認識するようにする
      XSS対策と一緒

    • JSONJavaScript として解釈不可能、あるいは実行時にデータを読み込めないようにする
      JSON の配列ではなくオブジェクトで返す。もしくはJSON ファイルの先頭に無限ループなどを仕込んで読み込み時に処理が進まないようにする。

セキュリティ関係のHTTPヘッダ

X-Content-Type-Options

X-Content-Type-Options: nosniff

上記のヘッダをつけるだけで、勝手にメディアタイプを解釈されることを防ぐ

X-XSS-Protection

ブラウザがXSS の検出、防御機能を備えている。 IE8 以上では以下のヘッダを送ることで、設定を有効化できる。 他ブラウザでは上書きできない(デフォルト有効)

X-XSS-Protection: 1; mode=block

X-Frame-Options

フレーム(FRAMEとIFRAME 要素)内で読み込まれることを防いでくれる。

X-Frame-Options: deny

Content-Security-Policy

読み込んだHTML内のIMG 要素、SCRIPT 要素、LINK 要素などの読み込み先としてどこを許可するのかを指定するためのヘッダ。
API の場合は要素を指定することがないため、直接は効果がないかも。

Contet-Security-Policy: default-src 'none'

Strict-Transport-Security

あらかじめこのサイトはHTTPS でのアクセスが必須、ということを記録させておくためのヘッダ。

Strict-Transport-Security: max-age=15678000

max-age で指定された期間中は、同じホストへのアクセスはHTTPが指定されていても、HTTPSを使うようにする。

Public-Key-Pins

SSL 証明書が偽造されたものでないかチェックする。
使い方は以下を参照。

Public Key Pinning - Web セキュリティ | MDN

Set-Cookieヘッダとセキュリティ

セッション管理にクッキーを使用する場合にセキュリティを向上させるために、 Secure、HttpOnly 属性を付与する。

Set-Cookie: session=XXXX; Path=/; Secure; HttpOnly
Secure HttpOnly
HTTPSの通信しかクッキーを送らない設定 クッキーがHTTP の通信のみで使われ、JavaScriptなどのスクリプトではアクセスできない設定

大量アクセスへの対策

そのサービス特性に基づき、ユーザ、IPごとなどの基準を設けて最大アクセス回数(レートリミット)を決める。

制限値を超えてしまった場合の対応

  • HTTPステータス 429 を返す。
  • エラーの詳細をレスポンスに含める。
  • Retry-After ヘッダを使って次のリクエストをするまでにどれくらい待てばよいかを指定する

レートリミットをユーザに伝える

以下のHTTP ヘッダを独自に作成して使う。

ヘッダ名 説明
X-RateLimit-Limit 単位時間あたりのアクセス上限
X-RateLimit-Remaining アクセスできる残り回数
X-RateLimit-Reset アクセス回数がリセットされるタイミング

リーダブルコード ~より良いコードを書くためのシンプルで実践的なテクニック~ エンジニアなら読むべき本の一つ!

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

昨日、今日でこの本を読んだ。
非常に読みやすく、遅読の自分でもすぐに読み終わることができた。

書いてある内容は保守性を気にしている人やリーダブルコードを意識している人にとっては、 新しいものではないが、非常に重要なことを整理してくれる。

とくに自分としては「短いコードを書く」という章を読んではっとした。
正しくてきれいなコードを書いたとしても、 その機能が過多で使われないならリーダブルコードにはならないのだ。
そう!必要な機能を見極めて、簡単に(=短いコードで)実装をするのが良いのだ。

自分のために各章のタイトルだけ書いておく。
自分としての目標はタイトルを見て、自分がなにをすべきか分かるようになるまで繰り返し読むことだ。

  1. 理解しやすいコード
  2. 名前に情報を詰め込む
  3. 誤解されない名前
  4. 美しさ
  5. コメントすべきことを知る
  6. コメントは正確で簡潔に
  7. 制御フローを読みやすくする
  8. 巨大な式を分割する
  9. 変数と読みやすさ
  10. 無関係の下位問題を抽出する
  11. 一度に1つのことを
  12. コードに思いを込める
  13. 短いコードを書く
  14. テストと読みやすさ 15.「分/時間カウンタ」を設計・実装する

WEB API The Good Parts 5章の自分的まとめ

続きで、今回は以下の本の5章を自分的にまとめたいと思う。

Web API: The Good Parts

Web API: The Good Parts

もしAPI を開発している、もしくはAPI を日々触っているのであれば、 是非買って読んでみてほしい。

設計変更をしやすいWeb API を作る

今回は重要なポイントは多いが、疑問や問題点はなかったので、 各章を一言でまとめて終わろうと思う。

  • 設計変更のしやすさの重要性

    • 外部に公開しているAPIの場合
      利用者が多く、利用者がきちんと対応できるようにするなどAPI の変更は超大変!
    • バイルアプリケーション向けAPI の場合
      外部公開よりは楽だが、それでもOSのバージョンがあげられないなどAPI の変更は結構大変!
    • ウェブサービス上で使っているAPI の場合
      ウェブも自分のものなので楽だが、それでもキャッシュされているなどちょっと大変!

      そうじて「一度公開をしたWeb APIの仕様を変更するのはいずれにせよ問題が発生する危険性がある」ということ。

  • API をバージョンで管理する
    以下のパターンがあるが、一般的に使われるのはURIにバージョンを埋め込む方法。

    • URI にバージョンを埋め込む
    • バージョンをクエリ文字列を入れる
    • メディアタイプでバージョンを指定する

  • バージョン番号をどうつけるか
    以下が一般的。

メジャーバージョン マイナーバージョン パッチバージョン
後方互換性のない変更が行われた場合 後方互換性のある機能変更、あるいは特定の機能が今後廃止されることが決まった場合 ソフトウェアのAPI に変更がないバグ修正などを行った場合


  • (メジャー)バージョンを変える際の指針
    後方互換性を保つことが可能な変更は可能な限り、同じバージョンでのマイナーバージョンアップで対応し、 バージョンをあげるのはどうしても後方互換性を保ったまま修正を行うことが難しい変更を加えなければならないときのみ、メジャーバージョンをあげる。

  • API の提供を終了する
    Twitter の例のように以下の対応が必要!

    • 新しいAPI の告知
    • API を廃止するという継続的な告知
    • Blackout Test と呼ばれる一時的に旧API を停止してアクセスできないようにするテスト

  • あらかじめ提供終了時の仕様を盛り込んでおく
    提供終了した場合に例えば410エラー(Gone)を返す、など提供終了時の仕様を決めておく。

  • 利用規約にサポート期限を明記する
    一定期間はAPI のバージョンをサポートする、と名言する。 サービスの目的により、保証とサービス提供の範囲の明確化が重要な場合は有効。

  • オーケストレーション
    汎用的なAPI はどうしても使い方が煩雑になる。 例えば1つのアクションを行うのに複数のAPI にアクセスしなければならなかったり、不要なデータを受け取らなければならず ペイロードが大きくなってしまう。

    サーバ側の汎用的なAPI とクライアントの間に"Client Adapter Code"を実行するオーケストレーション層をはさんで、 さまざまなデバイスに対応できるようにする。

    このオーケストレーション層を作成するのはクライアント側のエンジニアで、 クライアント側のデバイス機能やリリースサイクルに合わせて、エンドポイントを修正できる。

    Netflix のブログ記事 The Netflix Tech Blog: The Netflix Dynamic Scripting Platform によれば、
    しっかりとしたデバイス開発者用エンドポイント管理ツールが提供されており、 いわば社内専用PaaS として提供されているらしい。

さすがNetflix、はうらやましい限りですね!

WEB API The Good Parts 4章の自分的まとめ

前々回からの続きで、今回は以下の本の4章を自分的にまとめたいと思う。

Web API: The Good Parts

Web API: The Good Parts

もしAPI を開発している、もしくはAPI を日々触っているのであれば、 是非買って読んでみてほしい。

HTTP の仕様を最大限利用する

4.2 ステータスコードを正しく使う

この章についてはHTTP のステータスコードを解説しているが、 内容はwiki を見るだけで十分。

HTTPステータスコード - Wikipedia

4.4 メディアタイプの指定

メディアタイプとはデータ本体の形式を表すもので、 リクエスト時にはレスポンスがこの形式でほしいと指定する場合はAccecptヘッダー、 リクエスト、レスポンス時に送信するデータ本体はこの形式であると指定する場合はContent-Typeヘッダーで表す。

代表的なメディアタイプは以下の表を参照する。

メディアタイプ データ形式
text/plain プレーンテキスト
text/html HTML 文書
application/xml XML 文書
text/css CSS 文書
application/javascript JavaScript
application/json JSON 文書
application/rss+xml RSS フィード
application/atom+xml Atom フィード
application/octet-stream バイナリデータ
application/zip zip ファイル
image/jpeg JPEG 画像
image/png PNG 画像
image/svg+xml SVG 画像
multipart/form-data 複数のデータで構成されるウェブフォームデータ
video/mp4 MP4 動画ファイル
application/vnd.ms-exel Excel ファイル
application/x-msgpack MessagePage
application/x-yaml YAML
application/x-plist プロパティリスト
application/x-www-form-urlencoded HTMLのフォームデータ

トップレベルのapplication, text が混同しやすいが、 appllication のほうが主流。
plain, html ,css 以外はapplication となる。

サブタイプがx-で始まるものはIANA に登録されていない、 もしくはもしくは過去登録されていなかった歴史的経緯が残っているもの。

自分で作る場合は以下の表を参考にするとよい。
いまはあまりx-は推奨されないよう。。。だが一般的にはx-を付与している。

ツリー名 接頭辞
Standards tree(標準ツリー) なし
Vendor tree(ベンダツリー) vnd
Personal tree(パーソナルツリー) prs.
Unregistered tree(未登録ツリー) x.

vnd.ms-excelマイクロソフトというベンダーのexcel という意味。
ただし、通常は以下のように会社名を入れるらしい。

  application/vnd.companyname.awesomeformat

JSONXML を用いた新しいデータ形式を定義する場合は+ で記載する。
例えばXML をベースとしたRSSフィードの場合は以下のようになっている。

  application/rss+xml

4.3 キャッシュとHTTP の仕様

キャッシュタイプ タイプ説明 対応ヘッダ 説明
Expiration Model(期限切れモデル) あらかじめレスポンスデータに保存期間を決めておき、期限が切れたら再度アクセスをして取得する Expires: Fri, 01 Jan 2016 00:00:00 GMT 指定した時間以降はキャッシュ切れと判断する
Cache-Control: max-age=3600 指定した秒数が経過した以降はキャッシュ切れと判断する
Validation Model(検証モデル) いま保持しているキャッシュが最新であるかを問い合わせて、データが更新されていた場合に取得する response:Last-Modified: Tue, 01 Jul 2014 00:00:00 GMT → request:If-Modified-Since:Tue, 01 Jul 2014 00:00:00 GMT 最終更新時刻を記述する。時刻が一致すれば最新キャッシュと判断する。
response:ETag: "1234567890123456" → request:If-None-Match:"1234567890123456" 任意の文字列。通常はハッシュ値など。値が一致すれば最新キャッシュと判断する。
キャッシュさせない キャッシュを全くさせたくない場合に用いる Cache-Control: no-cache キャッシュしない

キャッシュを行う際に、URI 以外にどのリクエストヘッダ項目をデータを一意に特定するために利用するか特定するために、Varyヘッダを指定する。
例えば、Accept-Language: jaAccept-Language: en を指定して送信されたデータは異なるものになるため、 Vary: Accept-Language と指定する。

4.5 同一生成元ポリシーとクロスオリジンリソース共有

XHTTPRequest では異なるドメインに対してアクセスを行い、レスポンスデータを読み込むことができない。 これは同一生成元ポリシー(Same Origin Policy)というセキュリティ上のポリシーによるものである。

これを回避するために、クロスオリジンリソース共有(CORS:Cross-Origin Resource Sharing)がある。

実装方法はSpring-Bootならここを参考にしてほしい。

Getting Started · Enabling Cross Origin Requests for a RESTful Web Service

実装ができればこのような動きになる。

リクエストヘッダ サーバ処理 レスポン
Originヘッダに送信元のドメイン 許可する一覧かチェックする 許可されている場合:Access-Control-Allow-Originヘッダに許可されたドメイン
許可されてない場合:403 エラー

CORS にはプリフライトリクエストというものがあり、 これはリクエストを行う前にそのリクエストが受け入れられるかどうかを事前にチェックするもので、 OPTIONS メソッドを使ってリクエストすると、許可されているメソッドやヘッダーが表示される。

キャッシュヘッダの実装

今回、全然使ったことがないヘッダはキャッシュのところ。
なので、自分で実装を試してみた。

環境

  • Java 1.8
  • Spring Boot 1.3.6

そもそもヘッダー関連の実装って?

そう、レスポンスに毎回同じ設定をするのって大変だなぁ。。。
Spring-Boot でカバーしてないかなぁ?って思ったら、カバーしてました!

Spring Security
Spring Security Reference

さっそくpom.xml に追加してみる。

 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
    </dependencies>

ここからはSpring Security 初心者の私につきあってもらいたい。。。

適当にAPI を作成してアクセスしてみると、Basic 認証が表示され、アクセスできない。。。

f:id:AHA_oretama:20160710164704j:plain

なんてこった。。。

Spring Security のページを見ると大半がAuthentication, Authorization とかについて書かれている。
デフォルトでBasic 認証が有効になってしまうのか。。。

一つ勉強になったところで、Basic 認証を無効にしてみる。

以下のクラスを追加することで、Basic 認証していたコードを上書きし、 全てのリクエストを許可するようになる。

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().permitAll();
    }

}

@EnableWebSecurityWebSecurityConfigurer を設定するときのアノテーションで、 WebSecurityConfigurerAdapterconfigure(HttpSecurity http) がデフォルトでBasic 認証をしているコードとなるので、 それをOverrideして上書きする。 ここではBasic 認証をしている親クラスのメソッドsuper.configure(http) を呼び出してはいけない。

Sping Security を導入したことで、レスポンスヘッダーが以下のようになって、 API のデフォルトのキャッシュは無効になっている。

Cache-Control:no-cache, no-store, max-age=0, must-revalidate
Content-Type:application/json;charset=UTF-8
Date:Sun, 10 Jul 2016 08:00:56 GMT
Expires:0
Pragma:no-cache
Server:Apache-Coyote/1.1
Transfer-Encoding:chunked
X-Content-Type-Options:nosniff
X-Frame-Options:DENY
X-XSS-Protection:1; mode=block

Pragma:no-cache は古いヘッダーのようで、Cache-Controlが効かないときの予備だそう。
それ以外にもXSS対策などもついている。

では、URI ごとのヘッダーの設定は?

まずは独自のヘッダーを記述するStaticHeadersWriter を継承したクラスを作成する。

public class CacheControlHeadersWriter extends StaticHeadersWriter {

    /**
     * Creates a new instance
     */
    public CacheControlHeadersWriter() {
        super(createHeaders());
    }

    private static List<Header> createHeaders() {
        List<Header> headers = new ArrayList<Header>();
        headers.add(new Header("Cache-Control", "Cache-Control: max-age=3600"));
        headers.add(new Header("Pragma", "cache"));
        return headers;
    }
}

ここではmax-ageを使用して1日、キャッシュを保持させるようにしている。

さきほどのSecurityConfigクラスに対象のURIパスを指定して、 一度キャッシュを無効にしてから上記のCacheControlHeadersWriter を追加する。

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().permitAll();

        CacheControlHeadersWriter headersWriter = new CacheControlHeadersWriter();
        http.antMatcher("/api/v1/prefectures/**").headers().cacheControl().disable().addHeaderWriter(headersWriter);
    }

}

これで特定のURI のときだけキャッシュが効くようになる。

ではではChrome で確かめてみよう!! あれ?効かない。。。

調べたらなんと!同じタブで直接アクセスするとキャッシュは効かないという!!

iis - Is Chrome ignoring Control-Cache: max-age? - Stack Overflow

別タブなら効くというなんということだ!
親切すぎて、逆にはまりやすいポイントになった!

たしかに別タブならキャッシュが効く。よかった。。。

RestTemplate によるキャッシュは?

RestTemplate はSpirng でAPI へリクエストを送信するためのクラスである。
このクラスでキャッシュを判別してキャッシュが有効であればそのキャッシュを利用する、ということをしたい。

しかし、どうやらまだ未対応なようだ。。。

[SPR-5821] HTTP cache and conditional requests support in RestTemplate - Spring JIRA

ただ他のGitHub に対応するためのMaven があるようだ。

GitHub - jirutka/spring-http-client-cache: A very simple HTTP cache for the Spring’s RestTemplate.

今回は上記は使用しないが、今後のSpring-Bootの開発に期待している。

【2016/08/02追記】Validation Model(検証モデル)

検証モデルの場合は簡単でSpring にその機能が提供されている

 @RequestMapping(value = "/get", method = RequestMethod.GET)
 public String myHandleMethod(WebRequest webRequest, Model model) {
    long lastModified = // application-specific calculation
    if (request.checkNotModified(lastModified)) {
      // shortcut exit - no further processing necessary
      return null;
    }
    // further request processing, actually building content
    model.addAttribute(...);
    return "myViewName";
}

上記のようにcheckNotModified を呼ぶだけで、
更新があった場合はLast-Modifiedヘッダーをつけてくれ、 更新がなかった場合はHTTPステータスを302 にして返却してくれる。

サンプル

いつものように作成したリポジトリを公開しているので、参考にしてほしい。

github.com