三連休の初日というタイミングにも関わらず Kanazawa.rb の meetup で真面目に初めてのツール Pact を試し、現状の報告をするという真面目っぷり。今日は Pact の Provider 側の動作が一応分かったので現時点でのまとめを書いておく。
※ 実際にはこのあとさらにもう一回練習したうえで見えてきたことも混ざってるんだけど、そこはそれ
全体像
+------------------------------------------------+
| consumer |
| |
| spec/ |
| +- service_provider/ |
| | +-pact_helper.rb |
| | +- {service_name}_{consumer_name}_spec.rb |
| +- pacts/ |
| +- {pactfile} |
+------------------------------------------------+
(interaction = request ↓ + response ↑ )
+-------------------------------------------+
| provider |
| |
| spec/ |
| +- consumer_name/ |
| | +-pact_helper.rb |
| | +- {service_name}_provider_states.rb |
| +- pacts/ |
| +- {pactfile} |
+-------------------------------------------+
登場する用語
- consumer
- provider
- mock
- mock_service
- interaction
- given == provider_state
- upon_receiving == description
- with == request
- will_respond_with == response
consumer が API の client 側、provider が API の server 側、そして request と response の対が interaction で、これが基本のセット。
client ( consumer ) に必要な機能
一つ大きな特徴というか、そもそもこれが狙いなんだけど、consumer のテストは実際のサーバには繋ぎにいかない。そのために consumer の通信先、API の endpoint を自由にスイッチさせることのできる機能が必要になる。 これを使って Pact の consumer のテストの際には endpoint を mock_service に向けてテストを行う。
ちなみに Pact の example に使われているのは HTTParty というライブラリで、これは consumer クラスに HTTP 関連のクラスメソッドを include する。endpoint の設定には base_uri というメソッドを用いる。
jnunemaker/httparty: Makes http fun again!
mock_service の正体
想像通りの Rack アプリ。spec を流す際に裏で Rack アプリを立ち上げて、立ち上がるまで待って、RSpec の DSL で書いたリクエストを pact-support の中で組み立て直している。
一貫しているのだが、なかなかそれに気付けなかった点
特に自分がなかなか飲み込めなかった点を書いておく。Pact の考え方はちゃんと整理されているのだが、用語ばかりが先に立って考え方がなかなか入ってこない部分があったので、そのメモを並べていく。
interactionの相手をまず特定してからテストを書くこと
Pact でのテストは まず interaction の相手を特定するところから始まる というのを覚えておくと理解が早い。
Pact のテストは基本的には RSpec ベース1ということで spec/ 以下に、ファイルを置いていくんだけど、例えば consumer 側のテスト(これが provider 側では contract になる)を書きたい2場合は
spec/
+- service_providers/
+- pact_helper.rb
を作るところから始まる。consumer が contract を書きたい provider を特定するところから始まるのだ。だから consumer 側のテストは service_providers/ というディレクトリの中に入る。(はず)
同様に、サーバ側のテストは
spec/
+- service_consumers/
+- pact_helper.rb
という名前で、自分(provider)の contract を定義している consumer を特定して行く格好になる。
provider, consumer, provider_state の名前重要
基本的には consumer で contract を書いて → provider を verify する順でテストが進んでいくのだが、この時にどことどこの名前を合わせておく必要があるのかを理解していないとちゃんと動かない。
しかも名前が合っていないというエラーが出るわけではないのでますます注意が必要だ。
まず consumer 側で contract を書いていくわけだが、その時に出てくるのが
Pact.service_consumer {consumer_name} do
has_pact_with {provider_name} do
mock_service {mock_name} do
port xxxx
end
end
end
で
- contract を書こうとしている consumer の名前
- 次に interaction を行う provider の名前
- 最後に mock service の名前を決め、どの port で動かすかを決める
という形になっている。これで実際に contract を書く時には
{mock_name}.given({provider_state}).
with(request params).
will_respond_with(respose params)
という mock の定義を行うわけだが、ここで mock_name が一致していないとちゃんとサーバに繋がらないのでテストを行うことができない。
provider_state というのは provider がどんな条件の時にどういう動作をするかを記述するためのもの。実際の provider_state の記述は以下のような感じになる。
Pact.provider_states_for {provider_name} do
provider_state {provider_state} do
no_op
end
end
特に何も準備しなくて済むならよいが、DB の準備やら何やら必要な場合はここで state を再現する。
例えば GET / POST / PUT / DELETE のすべてにおいて、「すでに該当するデータが存在するかどうか」は一つの重要な state として挙げられるだろう。それによって API の response が変わるならばそれを state として記述していくことで、より実践的な contract になっていく。
Ruby以外の場合
自分が Pact に注目しているのは『マイクロサービスアーキテクチャ』で勧められているから、そしてクックパッドが取り上げていることと自分が Rubyist であるからだが、Ruby 以外の WebAPI も Pact でテストは可能である。
consumer 側
consumer の mock の動作を他の言語や RSpec 以外の DSL で記述するためのツールがいくつかある。例えば以下のように
- mopoke/pact-php
- standalone の ruby 版 pact-mock-service に依存
- DiUS/pact-consumer-js-dsl: A Javascript DSL for creating pacts.
- 同じく
- pact-foundation/pact-node: A wrapper for the Ruby version of Pact to work within Node without any complicated process.
- 全面的に Node.js で再実装
以下を見ると他にもいろいろ充実しつつあります。
..NET も Go も見える。
provider 側
provider 側も上のリンク先から探してもいいし、そもそも Pact の mock service は Rack アプリなんだから Rack で工夫できるよねということで
こんなのもある。これは rack-reverse-proxy を使って Rack アプリのテストをしてるフリしながら別サーバにリクエストを投げさせてテストする 格好になるので、アプリ側はどんな言語で書いてもオッケー。その代わりテストは Ruby で書いてね、という寸法。
provider_state の準備方法はあれこれ工夫が必要になりそうではありますが…。
[2016-07-31 追記] ある程度の受け入れテストはこれでできそう。厳密なものはクライアント側の方を集めにしないとダメかな?
Pactがあれば他のテストが不要になるわけではないが責務の分割を考えやすくなる
最後に今のところの理解のまとめと感想を。
Pact は API の interaction と contract に注目した一種のテスティングフレームワークであり、逆にそれ以外のところには関知しない。
すごく初歩的なところで言えば endpoint の切り替え機能。mock に向けられるようにしておくというのはすでに書いたが、mock じゃなくて production と staging の切り替えができるようにしたい場合もあるだろう。この機能は provider と consumer の間で取り決める contract の範囲外だ。したがって RSpec なり Minitest なり他のテストと同じように準備して同じようにテストする。
また API client 側であれば consumer のテストとは別に Model のテストも必要になるだろう。API client という名前では曖昧になってしまう責務が consumer としてくくり出されることで、逆にテストも実装もやりやすくなるように感じた。
これまで自分の書いたものはあまりそういうことを意識していなかったように思う。サーバサイドはすでにフレームワークが充実していて request を受け取り終わったあとの処理を書くことに特化しているので、リクエストを受け取る処理とその後の処理に分割するということを普段の実装では意識することがない。クライアントサイドも公開されている API に対してだけ書くか、ごく単純な GET くらいだったので、基本的には現物合わせをするだけで、request / response だけを責務としてテストするということはそんなに考えていなかった。
Pact はちょっと準備が面倒なところはあるし Ruby を普段使っていないとさらに面倒なところはあるが、なんとか wrap して使うことはできるので、今後は積極的に使っていくことでアプリを無駄に複雑にせずに済みそうな気がしている。
良いものを知った。
勇気づけられたやりとり
@wtnabe そうですね。あの辺はどれも重量級でローコンテキストな状況向けなので、内部APIには向かないと思います。Pactはもっとハイコンテキストなときの良い解法ですし、もう一つ考慮すべきは内部と外部APIの異なる要求を区別して外部との境界にfaçadeを置くことだと思います
— Yuki Yugui Sonoda (@yugui) July 19, 2016
なかなかこういうの相談できる人いないので、こういうツッコミはとても助かります。