開発効率向上のためのrunn活用

GMOメイクショップ コアグループ エンジニアの越川です。 直近の開発でAPIの開発がありました。シナリオテストを導入したかったので、今回の案件で導入してみました。 導入したruunが便利だったので、記事にさせていただきました。

1. 動機・モチベーション

シナリオテストに関して、前々から、導入検討していました。 弊社の開発環境ではCI/CDが導入されているため、Githubのブランチへのマージを契機に Actionsが自動でECSのコンテナにデプロイしています。 開発者はマージだけで済むので、便利ではあるのですが、 開発環境での動作確認は開発者に任せられているので、動作確認の強度に個人差があり、 ごく稀に、デプロイを失敗しているのを見逃されるケースや 他チームのサイレント修正によってトラブルが発生するケースがありましたので、 E2Eテストを実施して、基本的なシナリオはデプロイ後も問題なく動いていることは担保したいと考えていました。

2. 構成

構成イメージ

イメージの通りですが、ECSのデプロイを待ち合わせてからシナリオテストする必要があります。

3. runnについて

github.com

golangのテストヘルパーとして組み込めたり、様々な利点・特徴があるツールだと思いますが、 今回の案件では、以下の簡易さをメリットに感じて、採用しました。

4. 実装

DBの初期データを調整したあとにsample1APIを叩いたレスポンスを使用して、sample2APIをコールするサンプルになります。

desc: 前回作成した情報を削除する
runners:
  db: mysql://接続情報
vars:
  sample: "{{ parent.vars.sample }}"
steps:
  deleteRecode:
    desc: "作成した情報を削除する"
    db:
      query: |
        DELETE FROM sample WHERE sample = '{{ vars.sample }}' LIMIT 1;
desc: サンプルAPI1
steps:
  sample1:
    req:
      /api/v1/sample/1:
        post:
          headers:
            Content-Type: application/json
          body:
            application/json: "{{ vars.request }}"
    test: |
      # ステータスコードが200であること
      current.res.status == 200 &&
      # サンプル情報が取得できたこと
      current.res.body.sample != null
desc: サンプルAPI2
steps:
  sample2:
    req:
      /api/v1/sample/2:
        post:
          headers:
            Content-Type: application/json
          body:
            application/json: "{{ vars.request }}"
    test: |
      # ステータスコードが200であること
      current.res.status == 200 &&
      # サンプル2情報が取得できたこと
      current.res.body.sample2 != null
{
  "sample1":"{{ .vars.sample1 }}"
}
{
  "sample2":"{{ .vars.sample2 }}"
}
desc: シナリオテストのサンプル
runners:
  req: https://リクエストするAPIのURL
vars:
  sample1: "sample1"
steps:
  initDB:
    desc: "テストデータ作成"
    include: init-db/init-db.yml
  sample1:
    desc: "sample1"
    include:
      path: ../parts/sample1.yml
      vars:
        sample1: "{{ vars.sample1 }}"
        request: "json://sample1-request.json.template"
  sample2:
    desc: "sample2"
    include:
      path: parts/sample2.yml
      vars:
        sample2: "{{ steps.sample1.steps.sample1.res.body.sample1 }}"
        request: "json://sample2-request.json.template"

sample2にsample1のレスポンスを値を渡して実行しております。 簡単にAPIチェーンが書けて素敵です。

5. GithubActionsに埋め込み

# シナリオテスト実行
 - name: scenario test run
    run: |
        runn run sample.yml --debug

これだけなので非常に簡単です。

6. 実行結果

少し抜粋していますが、正常時と異常時のレスポンスは以下のような形になります。

正常

-----START HTTP RESPONSE-----
HTTP/2.0 200 OK
Connection: close
Cache-Control: no-store
Content-Type: application/json; charset=UTF-8
Date: Wed, 03 Apr 2024 00:58:25 GMT
Vary: Origin
{"sample1":"sample1"}

-----END HTTP RESPONSE-----

異常

-----START HTTP RESPONSE-----
HTTP/2.0 400 Bad Request
Content-Length: 234
Cache-Control: no-store
Content-Type: application/json; charset=UTF-8
Date: Tue, 19 Mar 2024 02:54:07 GMT
Vary: Origin

{}

-----END HTTP RESPONSE-----
Run "test" on "sample1".steps.sample1
F

1) sample.yml 739c217dad3b629e00fa6335bf9aa2ab3e042cad
   └── parts/sample1.yml
  Failure/Error: test failed on "sample1".steps.sample1: condition is not true
  
  Condition:
    current.res.status == 200
    ├── current.res.status => 400
    ├── 200 => 200
    

7. まとめ

入れてみての感想ですが、予想以上に役に立ってます。 開発中の案件だったので、簡単なコーディングミスや、認識齟齬でデプロイ後にテストが落ちたの検知したり、 他チームの変更に巻き込まれたりに気づけて、少し安心感を持って開発ができるようになりました。