2016-07-18

PactのProvider側の検証ができたのでいったんまとめ

三連休の初日というタイミングにも関わらず Kanazawa.rb の meetup で真面目に初めてのツール Pact を試し、現状の報告をするという真面目っぷり。今日は Pact の Provider 側の動作が一応分かったので現時点でのまとめを書いておく。

realestate-com-au/pact: Enables consumer driven contract testing, providing a mock service and DSL for the consumer project, and interaction playback and verification for the service provider project.

※ 実際にはこのあとさらにもう一回練習したうえで見えてきたことも混ざってるんだけど、そこはそれ

全体像

+------------------------------------------------+
| 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

  1. contract を書こうとしている consumer の名前
  2. 次に interaction を行う provider の名前
  3. 最後に 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 で記述するためのツールがいくつかある。例えば以下のように

以下を見ると他にもいろいろ充実しつつあります。

Introduction · Pact

..NET も Go も見える。

provider 側

provider 側も上のリンク先から探してもいいし、そもそも Pact の mock service は Rack アプリなんだから Rack で工夫できるよねということで

bethesque/pact-provider-proxy: Allows pact verification against a running provider at a configurable base URL

こんなのもある。これは 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 して使うことはできるので、今後は積極的に使っていくことでアプリを無駄に複雑にせずに済みそうな気がしている。

良いものを知った。

勇気づけられたやりとり

なかなかこういうの相談できる人いないので、こういうツッコミはとても助かります。

参考

  1. consumer 側は minitest で書く実装もある 

  2. 実際、最初にこれを行うのが肝要 

About

例によって個人のなんちゃらです

Recent Posts

Categories

Tool 日々 Web Biz Net Apple MS ことば News Unix howto Food PHP Movie Edu Community Book Security Text TV Perl Ruby Music Pdoc 生き方 RDoc ViewCVS CVS Rsync Disk Mail FreeBSD Cygwin PDF Photo Zebedee Debian OSX Comic Cron Sysadmin Font Analog iCal Sunbird DNS Linux Wiki Emacs Thunderbird Sitecopy Terminal Drawing tDiary AppleScript Life Money Omni PukiWiki Xen XREA Zsh Screen CASL Firefox Fink zsh haXe Ecmascript PATH_INFO SQLite PEAR Lighttpd FastCGI Subversion au prototype.js jsUnit Apache Trac Template Java Rhino Mochikit Feed Bloglines CSS del.icio.us SBS qwikWeb gettext Ajax JSDoc Rails HTML CHM EPWING NDTP EB IE CLI ck ThinkPad Toy WSH RFC readline rlwrap ImageMagick epeg Frenzy sysprep Ubuntu MeCab DTP ERD DBMS eclipse Eclipse Awk RD Diigo XAMPP RubyGems PHPDoc iCab DOM YAML Camino Geekmonkey w3m Scheme Gauche Lisp JSAN Google VMware DSL SLAX Safari Markdown Textile IRC Jabber Fastladder MacPorts LLSpirit CPAN Mozilla Twitter OpenFL Rswatch ITS NTP GUI Pragger Yapra XML Mobile Git Study JSON VirtualBox Samba Pear Growl Mercurial Rack Capistrano Rake Win RSS Mechanize Sitemaps Android JavaScript Python RTM OOo iPod Yahoo Unicode Github iTunes God SBM friendfeed Friendfeed HokuUn Sinatra TDD Test Project Evernote iPad Geohash Location Map Search Simplenote Image WebKit RSpec Phone CSV WiMAX USB Chrome RubyKaigi RubyKaigi2011 Space CoffeeScript Nokogiri Hpricot Rubygems jQuery Node GTD CI UX Design VCS Kanazawa.rb Kindle Amazon Agile Vagrant Chef Windows Composer Dotenv PaaS Itamae SaaS Docker Swagger Grape WebAPI Microservices OmniAuth HTTP 分析基盤 CDN Terraform IaaS HCL Webpack Vue.js BigQuery Middleman CMS AWS PNG Laravel Selenium OAuth OpenAPI GitHub UML GCP TypeScript SQL Hanami Document SVG AsciiDoc Pandoc DocBook Develop Jekyll macOS Node.js Vite Heroku Transformer AI Data Cloud Wasm