2022-11-22

インテグレーションテスト用ツールの調査まとめ

KUWASHIMA yuichiro

SmartShoppingでプログラマをしております桑島です。

序文

スマートショッピングではCTOとDevOpsチームを中心にエンジニア組織の生産性向上に取り組んでおり、その一環としてインテグレーションテスト導入のためツールの調査を行ないました。

調査対象ツール

インテグレーションテストについて検索してみると、Cucumberなどのフロントエンド側からテストする手法や、PostmanなどのAPIを直接実行する手法など考え方が様々のようですが、スマートショッピングではE2Eレベルのテストは別途行っているため、後者のツールについて検討しました。

PostmanについてはNewmanでコマンドラインから実行できることがわかっていましたが、Postmanから出力される定義ファイルの差分レビューなどに課題があるという記事が複数見られたので、対象から外しています。

インテグレーションテストツールを検索して上がってきた中から下記ツールについて調査しました。 どちらもGolang製でYAMLファイルでテストを定義するものになっています。

ツールの導入〜実行まで

テストシナリオとしては、APIでデータを投入後、結果を取得して値が合っていることを確認することを想定しています。

当記事では例としてtrelloのAPIを実行・検証していきます(たまたま手元で使っていたのと、エンジニアなら使ったことがありそうなので)

trello自体やtrelloのAPIについてはサイトやドキュメントを参照してください。

ということで、trelloのlist内にcardを作成してみて、正しく作成できているか確認するシナリオをそれぞれのツールで書いてみます。

runn

k1LoWさんが作ったツールで、基本的にはscenarigoと似ていますが、大きく違う特徴としては以下かなと思います。

  • 組み込みでSQLやコマンド実行もできる
  • 結果の検証は組み込み言語(antonmedv/expr)で行う
  • Golangのtest helperに組み込める

早速ですが、導入から定義ファイル、実行までみていきます。

導入

ここでは go install していますがhomebrewなどでも導入可能です。

go install github.com/k1LoW/runn/cmd/runn@latest

テスト定義(Runbook)

テスト定義ファイルはYAMLです。curl、grpcurlのコマンドを解析して出力できるらしいのですが、今回はうまくいかなかったので手で書いていきます。

List記法とMap記法がありますが、テスト結果の再利用などMap記法のほうがやりやすそうなので、Map記法で書いていきます。記法の違いについてはこちらの図がわかりやすいです。

以下がテスト定義ファイルの全体になります。APIキー、トークン、listのIDについては実際はファイル中に直接書いていますが、適宜置き換えてください。

desc: add new card
runners:
  req: https://trello.com/1
vars:
  key: {{APIキー}}
  token: {{APIキーで取得したトークン}}
steps:
  add:
    req:
      /cards?key={{ vars.key }}&token={{ vars.token }}&idList={{listのID}}&name=create%20module%20A&desc=create%20module%20A%20for%20serverside:
        post:
          body:
            application/json:
              body: ""
    test: |
      steps.add.res.status == 200 && steps.add.res.body.name == 'create module A'
  get:
    req:
      /cards/{{ steps.add.res.body.id }}?key={{ vars.key }}&token={{ vars.token }}:
        get:
          body: null
    test: |
      steps.get.res.status == 200 && steps.get.res.body.name == 'create module A'

まずrunnersでアクセス先を定義します。URL中にAPIバージョンなどあればここに含むことができます。gRPC/DB等アクセス先の判定はURLのプロトコルスキームで判定しているようです。

desc: add new card
runners:
  req: https://trello.com/1

varsではシナリオ中で再利用できる変数を定義できます。後述のstepsのなかで {{ vars.key }} で利用できます。

vars:
  key: {{APIキー}}
  token: {{APIキーで取得したトークン}}

stepsが実際のAPI実行部分です。下記2行目の add: はシナリオ1つごとの名前で、わかりやすくつけておくと、レスポンスの値を他のシナリオで利用するときに便利になりそうです。

下記3行目の req は runnners で定義したURLを参照しています。

trelloのAPIはクエリパラメータで各値を設定する形ですが、runnではJSONデータをPOSTすることを念頭に置いているようです。JSONでパラメータを送信する場合は中身をYAMLで記述できます。このAPIではPOSTデータは必要ないので空にしています。

steps:
  add:
    req:
      /cards?key={{ vars.key }}&token={{ vars.token }}&idList={{listのID}}&name=create%20module%20A&desc=create%20module%20A%20for%20serverside:
        post:
          body:
            application/json:
              body: ""

test: がAPIレスポンスの検証部分です。前述のとおり組み込み言語の式になっています。レスポンスの結果は steps.[シナリオの名前].res.statussteps.[シナリオの名前].res.body.name などで参照できます。

    test: |
      steps.add.res.status == 200 && steps.add.res.body.name == 'create module A'

前のシナリオの結果を別のシナリオで使い回すことも可能です。 List記法の場合は、steps[1].res.status のようにテストシナリオのインデックスで値を参照することになります。

    req:
      /cards/{{ steps.add.res.body.id }}?key={{ vars.key }}&token={{ vars.token }}:

実行

$ runn run new_card.yml                                               
add new card ... ok

1 scenario, 0 skipped, 0 failures

scenarigo

zoncoenさん作でメルカリ社内で使われてるようです。

実は弊社のBiosugar0が既に記事化しておりそちらを見てもらったほうが詳しい(もしかしたら日本一詳しいscenarigoの記事かも)のですが、一応書いていきます。

runnとの差異でいうと以下かなと思います。

  • 定義ファイルが宣言的(特に結果検証の部分)
  • APIから取得した値の再利用に変数への代入が必要

導入

$ go install github.com/zoncoen/scenarigo/cmd/scenarigo@v0.12.8
$ scenarigo config init

定義ファイル

テストの設定ファイル(senarigo.yaml) シナリオファイル(ディレクトリ)を列挙したりデバッグモードを設定したりできます。

schemaVersion: config/v1
scenarios:
  - new_card.yml

テストファイル(new_card.yml)の全体は以下の通りです。APIキーなどは適宜読み替えてください。

title: add new card
vars:
  key: {{APIキー}}
  token: {{APIキーで取得したトークン}}
steps:
  - title: add new card
    protocol: http
    request: 
      method: POST
      url: "https://trello.com/1/cards?key={{ vars.key }}&token={{ vars.token }}&idList={{listのID}}&name=create%20module%20A&desc=create%20module%20A%20for%20serverside"
    expect:
      code: OK
      body:
        name: 'create module A'
    bind:
      vars:
        id: '{{response.id}}'
  - title: get added card
    protocol: http
    request:
      method: GET
      url: "https://trello.com/1/cards/{{ vars.id }}?key={{ vars.key }}&token={{ vars.token }}"
    expect:
      code: OK
      body:
        name: 'create module A'

パーツごとに順番に見ていきます。

runnと同様に変数を定義して、シナリオ中 {{ vars.key }} で再利用できます。

vars:
  key: {{APIキー}}
  token: {{APIキーで取得したトークン}}

stepsはシナリオのリストになります。プロトコルやメソッドを宣言的に記述します。

steps:
  - title: add new card
    protocol: http
    request: 
      method: POST
      url: "https://trello.com/1/cards?key={{ vars.key }}&token={{ vars.token }}&idList={{listのID}}&name=create%20module%20A&desc=create%20module%20A%20for%20serverside"

値の検証は、結果として期待される値をYAMLで宣言的に書く形です。

    expect:
      code: OK
      body:
        name: 'create module A'

レスポンスの値を再利用したい場合は明示的に再代入します。

    bind:
      vars:
        id: '{{response.id}}'

実行

$ scenarigo run
ok      new_card.yml    1.204s

まとめ

以上、2つのツールの利用方法でした。

runnは動的、scenarigoは静的と、似ているようで違うフィロソフィーがあるなと感じました。

書き方については割と好みかなと思います。

機能的な面で言うと、runnのほうがいつの間にか CDP Runner: Control browser using Chrome DevTools Protocol (CDP) に対応していたり、APIレスポンスをOpenAPI定義と突合できたり、活発に機能追加されている印象です。

scenarigoについてはメルカリ社内でも使われていて比較的枯れているようなので安心感があります。特殊なことをしたい場合でもGo言語でのPlugin機能があるので、最悪そういう逃げ道もありそうです。

まだ導入には至っていませんが、引き続き生産性向上に取り組みたいと思っています。

最新の記事