🐕

Pact-Rubyによるコントラクトテスト

2025/01/11に公開

コントラクトテストとは、利用者とAPI提供者の間に契約(コントラクト)を定義し、利用者提供型のコントラクトテストを可能にする。
利用者提供型のコントラストテストは、利用者がやりとりしたい機能を実装するアプローチ。利用者は、API提供者にコントラクトを提供する。

なぜコントラクトテストを使うのか

OpenAPIはAPI提供者が提供する仕様書でドキュメントの役割となる。コントラクトテストは利用者とAPI提供者の間のAPI提供者が履行すべき仕様を定義でき、検証できる。

pact-rubyの図がわかりやすい。
コントラクトテストはテストを分割できる。

pact-rubyを導入する

コントラクトテストフレームワークはPact https://docs.pact.io/ を利用する。
Rubyの場合は、Pact-Ruby https://github.com/pact-foundation/pact-ruby のため、動かしてみる。

シナリオ例

pact-rubyにあるシナリオ例を動かす。
zoo-appが利用者側、animal-serviceがAPI提供者側となる。

利用者側となるzoo-appで、rspecを実行する。

> cd example zoo-app
> bundle install
> bundle exec rspec

zoo-app/spec/pacts/zoo_app-animal_service.jsonのファイルが作成される。これはAPI提供者が履行すべきコントラクトテストの仕様書となる。

API提供者側となるanimal-serviceで、pact:verifyを実行する。利用者側が作成したコントラクトに基づいてAPIが提供されているかの仕様を検証できる。

> cd example zoo-app
> bundle install
> bundle exec rake pact:verify
Verifying a pact between Zoo App and Animal Service
  Given there is an alligator named Mary
    a request for an alligator
      with GET /alligators/Mary
        returns a response which
          has status code 200
          has a matching body
          includes headers
            "Content-Type" which equals "application/json;charset=utf-8"
  Given there is not an alligator named Mary
    a request for an alligator
      with GET /alligators/Mary
        returns a response which
          has status code 404
  Given an error occurs retrieving an alligator
    a request for an alligator
      with GET /alligators/Mary
        returns a response which
          has status code 500
          has a matching body
          includes headers
            "Content-Type" which equals "application/json;charset=utf-8"

3 interactions, 0 failures

補記

pact-rubyの例ではSinatraが使われているが、https://github.com/pact-foundation/pact-ruby/pull/327 の対応が誤っている?ため、修正が必要。
現状だと、Host not permittedとなる。

下記に修正をする。

# lib/pact/provider/request.rb
        def headers
          request_headers = {}
-          request_headers.merge!('HTTP_HOST' => 'localhost') if defined?(Sinatra)
+          request_headers.merge!('HOST' => 'localhost') if defined?(Sinatra)
          return request_headers if expected_request.headers.is_a?(Pact::NullExpectation)

参考

https://developers.freee.co.jp/entry/introduction-of-pact

https://developer.mamezou-tech.com/blogs/2022/12/03/contract-test-with-pact/

https://technology.blog.gov.uk/2021/10/08/a-new-standard-of-testing-for-gov-uk/

https://github.com/alphagov/whitehall

Discussion