GoでどんなかんじでPactを使えるか書いていくよー
Pactについて、情報量はまだ少ない印象ですがその分公式は丁寧に説明してくれてる気がするので、そちらを一読することをオススメします。
必要なもの
- pact-ruby-standalone ... PactのCLIツール
- pact-go ... GoでPactを利用するために必要なSDK
- pact_broker-docker ... ConsumerとProviderとの間でPactを共有するためのツール
サンプルアプリ
この記事の中で使うサンプルアプリは👇にあります。
nihei9/dive-into-pact-go
サンプルアプリはレシピのサーバとレシピを見るためのCLIツールで、構造はこんなかんじです。
ビルド方法は👆のリポジトリをご覧ください。
dive-into-pact-go
├─ provider ............... レシピサーバ
│ ├─ cmd
│ │ └─ recipes
│ │ └─ main.go
│ └─ handler
│ ├─ handler.go
│ └─ handler_test.go ... Providerの検証を実装したUT
└─ consumer ............... CLIツール
├─ cmd
│ └─ recipes
│ └─ main.go
└─ client
├─ client.go
└─ client_test.go .... Consumerの検証を実装したUT
基本的な検証
ConsumerとProviderの検証だけならPact Brokerがなくてもできるようなので、まずはPact BrokerなしでPactをいじってみます。
GoからPactを利用するときはUTとして実装します。
解説に使用しているコードは完全なものではありません。前述の通りnihei9/dive-into-pact-goに動作するコードを置いています。
ではまずはConsumerから。
var pact dsl.Pact
func TestMain(m *testing.M) {
// Pactの設定
pact = dsl.Pact{
Consumer: "consumer name",
Provider: "provider name",
LogDir: "path/to/log/dir",
PactDir: "path/to/pact/dir",
LogLevel: "DEBUG",
}
// テスト実行
exitCode := m.Run()
// Pactの書き出しと後処理
pact.WritePact()
pact.Teardown()
os.Exit(exitCode)
}
func TestConsumer(t *testing.T) {
// ConsumerのUT
}
大枠としては👆のような形がテンプレとして利用できそうです。(公式の実装例を真似てます)
TestMain()
が入り口になっていて、その中で
- Pactの設定
- テスト実行
- Pactの書き出し
という順で処理していきます。
ちなみに、pact
をトップレベルで定義している理由は、pact
に対してinteractionを定義したりConsumerの検証を実行したりする都合上、UT全体で共有する必要があるためです。
Pactの設定ではまずはここに記載のものを押さえておけばよさそうです。
Consumer
とProvider
はそのままConsumerとProviderの名前です。何を指定してもOKなのでいい感じに命名しましょう
LogDir
とPactDir
もそれぞれログファイルとPactファイルの出力先となディレクトリのことです。
LogLevel
はPactの検証中に出力されるログのレベル指定で"DEBUG", "INFO", "WARN", "ERROR"
が指定できます。
内部でhashicorp/logutilsを使ってるようです。
TestMain()
以外のTest*()
にはPactでのConsumerの検証を実装していきます。
func TestConsumer(t *testing.T) {
// UTロジックを含む関数の定義
var testSushiExists = func() error {
c := New("localhost", pact.Server.Port)
_, err := c.GetRecipe("12345678")
if err != nil {
return err
}
return nil
}
// interactionの定義
pact.
AddInteraction().
// provider stateの設定
Given("Recipe exists").
// interactionの説明
UponReceiving("A request to get a recipe").
// Consumerからのリクエスト
WithRequest(dsl.Request{
Method: http.MethodGet,
Path: dsl.Term("/v1/recipes/12345678", "/v1/recipes/[0-9a-z]+"),
}).
// 期待するProviderからのレスポンス
WillRespondWith(dsl.Response{
Status: http.StatusOK,
Body: dsl.Like(map[string]interface{}{
"id": dsl.Like("12345678"),
"name": dsl.Like("Sushi"),
"ingredients": dsl.EachLike(map[string]interface{}{
"name": dsl.Like("rice"),
}, 1),
}),
Headers: dsl.MapMatcher{
"Content-Type": dsl.Term("application/json", `application\/json`),
},
})
// 検証
err := pact.Verify(testSushiExists)
if err != nil {
t.Fatalf("Error on Verify: %v", err)
}
}
Given()
はprovider stateを設定します。
WithRequest()
とWillRespondWith()
はそれぞれConsumerからのリクエストとProviderからのレスポンスの期待値を表します。
レスポンスの期待値はでは期待する値を固定値で指定することもできますが、多くの場合はそのドメインを指定することになりそうです。
その場合はLike()
のようなメソッドを使用して正規表現で期待値のドメインを表現できます。
続く……
検証の実行はこの後書くよ