6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

さくらインターネットAdvent Calendar 2024

Day 10

yamlでかけるシナリオテストツールrunnを使ってみた

Last updated at Posted at 2024-12-10

はじめに

この記事は、さくらインターネット Advent Calendar 2024 10日目の記事です。さくらのクラウドの開発に携わっている太田と申します。
今回は、業務で活用したシナリオテストツール「runn」をご紹介いたします!

runnとは?

Goのcobraで実装されたシナリオテストツール。

runnの特徴

一般的なシナリオテストツールとしてはKarateなどが知られていますが、runnはYAML形式でシナリオ(ランブック)を作成できる点が大きな特徴です。日本人の開発者が手掛けたツールであり、主に以下の理由から導入してみました。

  • 充実したドキュメント:公式資料がわかりやすく整理されており、導入のハードルが低い
  • 読者コミュニティの存在:質問や意見交換がしやすく、問題解決のスピードが速い
  • YAMLの記述性の高さ:直感的にシナリオを構築でき、メンテナンスも容易

インストール

homebrew tap

brew install k1LoW/tap/runn

Go

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

その他のインストール方法

他のインストール方法は以下に載っている。

runnのオプション

runnコマンドのオプション一覧は以下の通り。(runn -hで確認可能。)

オプション 説明
-h ヘルプ表示
-v バージョン表示
completion 補完スクリプトの出力
coverage カバレッジ出力
help コマンドの使い方の表示
list 指定したパターンにマッチしたシナリオを出力
loadt 負荷テスト
new curlからシナリオの自動生成
rprof プロファイリング
run シナリオテストの実行

バージョン確認

$ runn -v
runn version 0.123.0

curlやコマンドからシナリオを自動生成する

runn newを使うとcurlコマンドからrunnのシナリオを自動生成してくれます。

$ runn new curl https://www.google.com/
desc: Generated by `runn new`
runners:
  req: https://www.google.com
steps:
- req:
    /:
      get:
        body: null

curlだけでなく、grpcurlコマンドからも自動生成してくれる。(grpcurlコマンドからランブックを生成する|runn クックブック)

コマンドから生成することも可能。

$ runn new -- echo hello
desc: Generated by `runn new`
steps:
- exec:
    command: |
      echo hello

プロファイリング

runn rprof [PROFILE_PATH] [flags] でプロファイルすることができる。

rprof, rrprof, rrrprof, prof は全てrprofのエイリアス。

$ runn run foo.yml --profile # runn.profが作成される
$ runn rprof runn.prof
  runbook[サンプルテスト](sample.yml)               62535.97ms  
    steps[foo].include                                              58520.37ms  

                     ###### 省略 ######
  [total]                                                                      62536.28ms

補完機能

以下のコマンドでrunnの補完スクリプトを出力できる。対応しているshellは zsh, bash, fish, powershell

runn completion [your shell]

負荷テスト

runn loadt で負荷テストが可能。

オプション多いので、runn help loadtで確認してください。

$ runn loadt --load-concurrent 2 --max-rps 0 sample.yml

Number of runbooks per RunN....: 15
Warm up time (--warm-up).......: 5s
Duration (--duration)..........: 10s
Concurrent (--load-concurrent).: 2
Max RunN per second (--max-rps): 0

Total..........................: 12
Succeeded......................: 12
Failed.........................: 0
Error rate.....................: 0%
RunN per seconds...............: 1.2
Latency .......................: max=1,835.1ms min=1,451.3ms avg=1,627.8ms med=1,619.8ms p(90)=1,741.5ms p(99)=1,788.4ms

カバレッジ出力

Protocol BuffersやOpen APIを使っている場合は、 runn coverage でカバレッジを出力できる。

シナリオ出力

$ runn list *.yml
  id:      desc:                 if:  steps:  path
-----------------------------------------------------------------------
  455aafd  サンプルテスト           8  sample.yml
  58652b2  サンプルテスト2          4  sample2.yml

シナリオの実行

runn run sample.yml とするとシナリオを実行することが可能です。

runにはたくさんのオプションがあるので、自分が特に使用する一部だけ紹介します。(詳細はrunn help run)

オプション 説明
--env-file .envファイルの指定
--debug デバッグしてくれる
--verbose 実行時の詳細表示
--sample ランダム実行の数を指定する
--concurrent 並行処理の並行処理数を指定する

簡単なシナリオの例

hello world

まず、以下のhello.ymlを作成します。

hello.yml
desc: hello
steps:
  -
    dump: "'Hello world!'"

そして、hello.ymlを実行すると、Hello world! が出力できます。
stepsは必要項目ですが、-とすることで省略できます。

実行結果
$ runn run hello.yml
Hello world!
.

1 scenario, 0 skipped, 0 failure

GETリクエスト

sample.yml
desc: Getリクエストテスト
runners:
  req: https://jsonplaceholder.typicode.com
steps:
  getUser:
    desc: "userを取得する"
    req:
      /users/1:
        get:
          body:
            application/json: null
    test: |
      current.res.status == 200
実行結果
$ runn run sample.yml --verbose 
=== Getリクエスト (hello.yml)
    --- userを取得する (getUser) ... ok


1 scenario, 0 skipped, 0 failures

runbookごとに指定できる項目の一覧

項目名 説明
desc runbookの説明
runners エンドポイントなどの設定をする
steps 各ステップを定義する
labels ラベル
hostRules 名前解決の設定
vars 環境変数
secrets シークレット
debug デバッグの有無
interval ステップの間隔
if ステップを動作する条件
skipTest テストをスキップする
force 強制的にステップを実行する
trace ヘッダーとクエリにトレース用のトークンを追加する
loop ループの設定
concurrency 並列処理するキーの設定
needs 事前処理の設定

ソースコード : runn/runbook.go at 11ee56c1d4e6ab1d8866467ade0bb4939d95b702 · k1LoW/runn

runners

先ほどのGetリクエストの例のようにエンドポイントなどHTTPランナーの設定ができる。

desc: Getリクエストテスト
runners:
  req: https://jsonplaceholder.typicode.com
steps:
  getUser:
    desc: "userを取得する"
    req:
      /users/1:
        get:
          body:
            application/json: null
    test: |
      current.res.status == 200

reqの値は自由に変更可能。

desc: Getリクエストテスト
runners:
  api: https://jsonplaceholder.typicode.com
  my: dbuser:${DB_PASS}@hostname:3306/dbname
steps:
  getUser:
    desc: "userを取得する"
    api:
      /users/1:
        get:
          body:
            application/json: null
    test: |
      current.res.status == 200

また、各ランナーには以下の設定値が使用可能である。

設定値 説明
endpoint エンドポイント
notFollowRedirect trueでリダイレクトを無効化する
useCookie trueでCookieの自動保存を有効化
openapi3 openapiファイルを指定する

上記に挙げたもの以外にもカスタムCAと証明書X-Runn-Traceトレース用のHTTPリクエストにヘッダーを追加するgRPC ランナー: gRPC リクエストを実行するなどができる。

発展的な内容として、runnersにcc: chrome://newを設定することでCDPを使って、selenium みたいにブラウザを操作できるようになります。(参考 : CDPを使ってブラウザ操作してみる|runnチュートリアル)

vars

varsに変数を定義することができる。

desc: hello
vars:
  foo: "foo"
steps:
  hello:
    dump: vars.foo

文字列内で変数を展開する場合は{{vars.foo}}のように指定する

sample.yml
desc: hello
vars:
  foo: "foooo"
steps:
  cmd:
    exec:
      command: echo "{{ vars.foo }}"
      liveOutput: true
実行結果
$ runn run --scopes run:exec sample.yml
foooo
.

1 scenario, 0 skipped, 0 failures

secrets

指定した変数をマスクすることができる。

sample.yml
vars:
  foo: "foooo"
secrets:
  - vars.foo
steps:
  -
    dump: vars.foo
実行結果
$ runn run sample.yml
*****
.

1 scenario, 0 skipped, 0 failures

labels

ラベルを指定することができる。

labels:
  - foo
  - hoge
steps:
    # ・・・・

指定したラベルでRunbook実行時にフィルタリングが可能となる。

runn run sample.yml --label foo --label hoge

or

runn run sample.yml --label 'foo and hoge'

hostRules

名前解決ができる。

hostRules:
  localhost: 127.0.0.2:8000

debug

trueにするとdebugすることができる。runn run --debugと同じ。

debug: true
steps:
    # ・・・・

interval

以下のようにinterval:でstepの間隔を指定できる。

interval: 10
vars:
  foo: "foooo"
steps:
  step1:
    dump: vars.foo
  step2:
    dump: vars.foo

単位を省略した場合はsになる。sms, m の単位を明示して指定することも可能。

if

iftrue の時だけrunbookが実行される。

if: included
vars:
  foo: "foooo"
steps:
  step1:
    test: |
      vars.foo == "foooo"
  step2:
    test: |
      vars.foo == "foooo"

included を指定すると、異なるrunbookからコールされた時のみ includedtrue になり、実行される。

実行結果
$ runn run sample.yml --verbose 
=== [No Description] (sample.yml)
    --- (step1) ... skip
    --- (step2) ... skip


1 scenario, 1 skipped, 0 failures

スキップされたので、1 skippedとなる。

skipTest

skipTesttrue にするとテストが全てスキップされる。

skipTest: true
vars:
  foo: "foooo"
steps:
  step1:
    test: |
      vars.foo == "foooo"
  step2:
    test: |
      vars.foo == "foooo"
実行結果
$ runn run sample.yml --verbose
=== [No Description] (hello.yml)
    --- (step1) ... skip
    --- (step2) ... skip


1 scenario, 0 skipped, 0 failures

skipTestを使ってスキップした場合は、skippedに加算されない。

force

force: trueとすると全てのstepが強制的に実行される。

trace

trace: trueをつけると、ヘッダーとクエリにトレースするためのトークンが付けられる。

参考 : アクセスログからシナリオを特定する|runnチュートリアル

loop

以下2通りの書き方でどちらも、runbookが100回ループされる。

loop: 100

or

loop:
  count: 100

また、loopには count以外にも以下のオプションがある

オプション 説明 例 
until 終了条件 until: 'status == "ok"'
minInterval 最小間隔 minInterval: 0.01
minInterval 最大間隔 minInterval: 100

concurrency

concurrency: fooと指定すると、キーワードが同じrulebookを並行に実行できる。

needs

事前処理のrunbookを指定できる。

ランブック間に依存関係を設定する|runn クックブック

sample2.yml
vars:
  foo: "hello"
steps:
  step1:
    exec:
      command: |
        echo "{{ vars.foo }} world!"
    bind:
      greeting: current.stdout

sample.yml のneedsで sample2.yml を事前処理として呼び出す。

sample.yml
needs: 
  hoge: sample2.yml
steps:
  step1:
    dump: needs.hoge.greeting

実行すると、

実行結果
$ runn run --scopes run:exec sample.yml --verbose
=== [No Description] (sample2.yml)
    --- (step1) ... ok
=== [No Description] (sample.yml)
hello world!

    --- (step1) ... ok


2 scenarios, 0 skipped, 0 failures

steps

stepsの中に複数のstepをかく。

steps:
  step1:
    dump: "`foo`"
  step2:
    dump: "`hoge`"

stepごとの設定値

stepごとに設定できる項目

項目名 説明
desc stepの説明
dump 指定した値を出力する
exec コマンドを実行する
bind 変数をバインドする
test テストの検証
include runbookの再利用

dump

dumpを使うと、指定した値を出力できます。

runners:
vars:
  foo: "fooo"
steps:
  step1:
    dump: "`hoge`"  # hoge
  step2:
    dump: "2"       # 2
  step3:
    dump: vars.foo  # fooo

また、dumpには、以下のようなオプションがある。

設定値 説明 デフォルト
expr 入力 -
out 出力先 -
disableTrailingNewline falseにすると改行が入る false
disableMaskingSecrets falseのとき出力時に
シークレットをマスクする
false

例えば、以下を実行するとfoooと書かれたsample.outが生成される。

vars:
  foo: "fooo"
steps:
  -
    dump: 
      expr: vars.foo
      out: ./sample.out
      disableTrailingNewline: true # 改行なし

exec

コマンドを実行できる。

steps:
  -
    exec:
      command: echo "hello world!" # hello world!
      liveOutput: true
設定値 説明
command 実行するコマンド
stdin 標準入力
shell shellの指定(bashやsh)
background trueのときバックグラウンドでコマンドを実行する
liveOutput 出力する

また、コマンドの実行結果は出力がcurrent.stdoutもしくはcurrent.stderr、終了コードはcurrent.exit_codeに入っている。ゆえにdumpやbind、testする場合は、この値を使用する。

exec項目を含む場合はrunn run --scopes run:exec sample.ymlのように--scopesで権限を指定する必要がある。(環境変数RUNN_SCOPES=run:execでも可能)

bind

bindによって新しい変数に値を格納することができる。
bindした変数は同じrunbuook内で使用することができる。

vars:
  foo: "foooo"
steps:
  bindFoo:
    bind: 
      foo: vars.foo
  printBindedFoo:
    dump: foo # foooo

ただし、bindした変数はdumpで使えるのは次のステップからである。

vars:
  foo: "foooo"
steps:
  bindFoo:
    bind: 
      foo: vars.foo
    dump: foo # null

testなどでは同じstepでも使える。

vars:
  foo: "foooo"
steps:
  bindFoo:
    bind: 
      foo: vars.foo
    test: foo == "foooo" # ok

test

test項目ではその名の通りテストできる。

vars:
  foo: "foooo"
steps:
  -
    test: vars.foo == "foooo"

testで複数比較する場合には、&&をつける。

desc: Getリクエストテスト
runners:
  req: https://jsonplaceholder.typicode.com
steps:
  getUser:
    desc: "userを取得する"
    req:
      /users/1:
        get:
          body:
            application/json: null
    test: |
      current.res.status == 200
      && current.res.body.id == 1
      && current.res.body.name == "Leanne Graham"

レスポンスはcurrentに入っている。現在のstepをi番目とすると、currentsteps["i番目のstep名"]のエイリアスとなっている。ゆえに、上記の場合はcurrent.res.statussteps["getUser"].res.statusは同値となる。また、previoussteps[i-1番目のstep名]のエイリアスとなる。ちなみに、step名がない場合は、steps[0], steps[1]・・・のようにindexが振り分けられる。
参考: ステップに名前を付ける|runnチュートリアル

include

includeを使うとRunbookを再利用することができる。

sample.yml
steps:
  -
    include: 
      path: sample2.yml
      vars:
        foo: "foooo"
    test: current.greeting == "foooo world!"

再利用するランブックにはif: includedをつけることで、再利用専用にすることができる。

sample2.yml
vars:
  foo: "{{ parent.vars.foo }}"
if: included
steps:
  -
    exec:
      command: |
        echo -n "{{ vars.foo }} world!"
    bind:
      greeting: current.stdout

これを実行すると、以下のようになる。

実行結果
$ runn run --scopes run:exec sample.yml --verbose
=== Generated by `runn new` (sample.yml)
    --- (0) ... ok
        === Generated by `runn new` (sample2.yml)
            --- (0) ... ok


1 scenario, 0 skipped, 0 failures

また、上記のsample2.ymlでparentによって親のvarsにアクセスして、代入している。これはvars.fooのデフォルト値になる。

vars:
  foo: "{{ parent.vars.foo }}"

ゆえに、親のfoosample.ymlのfooの値と異なる時は、includeで渡した変数が子のvars.foo`に代入される。

include:
  path: sample2.yml
  vars:
    foo: "foooo"

環境変数の読み込み

--env-fileをつけることで環境変数を渡すことができる。

runn run sample.yml --env-file .env.local

また、環境変数は以下の ${BASE_URL}のようにすると展開できます。

vars:
  endpoint: ${BASE_URL}

ここで、${BASE_URL-http://example.com}のように、-の後にデフォルト値を設定することができます。

ビルトイン関数

bool()

bool値にキャストする。

steps:
    - dump: bool(1) # true

urlencode()

文字列をエスケープして、URLクエリ内に安全に配置できるようにする

steps:
    - dump: urlencode("my/cool+blog&about,stuff") # my%2Fcool%2Bblog%26about%2Cstuff

compare()

2つの値を比較し、ブール値を返す。ignorePathsにはpath(..|select(type=="boolean"))という形式で指定する。

vars:
  groupA:
    dog: "bowwow"
    cat: "meow"
  groupB:
    chicken: "cock-a-doodle-doo"
    cow: "moo"

steps:
    - dump: compare(vars.groupA, vars.groupB) # false
    - bind:
        groupA:
          dog: "'bowwow'"
          cat: "'meow'"
    - dump: compare(vars.groupA, groupA) # true

diff()

2つの値の差分を出力する。ignorePathsはcompare()と同じ。

vars:
  groupA:
    dog: "bowwow"
    cat: "meow"
  groupB:
    chicken: "cock-a-doodle-doo"
    cow: "moo"

steps:
    - dump: diff(vars.groupA, vars.groupB)
実行結果
$ runn run sample.yml
  map[string]any{
-       "cat":     string("meow"),
+       "chicken": string("cock-a-doodle-doo"),
+       "cow":     string("moo"),
-       "dog":     string("bowwow"),
  }

pick()

第二引数以降に指定したキーに該当するものを抽出する。

vars:
  dog: "bowwow"
  cat: "meow"
  chicken: "cock-a-doodle-doo"
  cow: "moo"

steps:
    - dump: pick(vars, "dog", "cat") 
実行結果
{
  "cat": "meow",
  "dog": "bowwow"
}

omit()

第二引数以降に指定したキーに該当するものを削除する。

vars:
  dog: "bowwow"
  cat: "meow"
  chicken: "cock-a-doodle-doo"
  cow: "moo"

steps:
    - dump: omit(vars, "dog","cat") 
実行結果
{
  "chicken": "cock-a-doodle-doo",
  "cow": "moo"
}

merge()

第一引数を第二引数にマージする。

vars:
  groupA:
    dog: "bowwow"
    cat: "meow"
  groupB:
    chicken: "cock-a-doodle-doo"
    cow: "moo"

steps:
    - dump: merge(vars.groupA, vars.groupB)
実行結果
$ runn run sample.yml
{
  "cat": "meow",
  "chicken": "cock-a-doodle-doo",
  "cow": "moo",
  "dog": "bowwow"
}

input()

標準入力。

steps:
    - dump: input("数字を入力してください", "") 
実行結果
数字を入力してください: 3
3

secret()

inputと似ているが、入力時に入力文字が表示されない。

steps:
    - dump: secret("パスワードを入力してください")

select()

select("メッセージ", ["候補1", "候補2", ・・・], "デフォルト値")のように使う。
候補から標準入力で選ぶ。

steps:
    - dump: select("a,b,cの中から選んでください。", ["a","b","c"], "a")
実行結果
a,b,cの中から選んでください。 [a]: c
c

basename()

ファイルのベースネームだけ抽出。

steps:
    - dump: basename("a.txt") # a.txt

time()

入力値をtime.Time()型に変換する

steps:
    - dump: time("2024/01/01") # 2024-04-01T00:00:00Z"

faker.*

ランダムな値を生成する。faker.Digit()faker.Date()

steps:
    - dump: faker.Email() # candelariostoltenberg@damore.net

APIシナリオテスト実践編

POSTリクエストの実装

POSTリクエストでユーザを作成するテストを実際に書いてみます。
まず、headerなど変化しやすいものは、認証のtokenだけ変数にするのではなく、まとめて変数にして外部から注入してあげてるのがおすすめです。

create-user.yml
desc: "ユーザ作成"
runners:
  req: "{{ parent.vars.endpoint }}"
vars:
  headers: "{{ parent.vars.headers }}"
if: included
steps:
  createUser:
    desc: "指定された内容でユーザ作成"
    req:
      /posts:
        post:
          headers: "{{ vars.headers }}"
          body:
            application/json: "{{ vars.request }}"
    bind:
      response: current.res

再利用するコンポーネントでは、test項目をつけないのようにするべきです。成功か失敗かはincludeする側で決めるためです。
以下では、練習のためリクエストのuseridの値はあえて変数で定義しています。

user-create-test.yml
desc: ユーザを作成する
vars:
  endpoint: "https://jsonplaceholder.typicode.com"
  headers:
    X-Requested-With: "XMLHttpRequest"
  userid: 100

steps:
  createUser:
    desc: ユーザを作成して成功する
    include:
      path: create-user.yml
      vars:
        request: "json://user.json.template"
        headers: "{{ vars.headers }}"
    test: |
      current.response.status == 201
      && current.response.body.userId == 100

postでjsonを外部から渡すときはuser.json.templateというGo Template形式のファイルにしなければなりません。Go Templateにするには、拡張子を.json.templateにする必要があるそうです。(参考 : JSONファイルにパラメータ埋め込みをしてみる|runnチュートリアル)

{
    "userId": {{ .vars.userid }},
    "title": "foo",
    "body": "hogehoge"
}

また、Go Templateファイルでの変数の展開は{{ .変数名 }}という形にします。知らないとハマるので気をつけてください!

go templateのパスの指定方法だが、絶対パスかincludeされる側(postリクエストが書かれているファイル)からみた相対パスを指定する必要がある。また、作業ディレクトリの親のディレクトリのファイルを見る場合は、--scopes read:parentと権限を指定する必要がある。

Github Actionで動かす

.github/workflow.yml
name: Test

on:
  workflow_dispatch:

jobs:
  run-test:
    runs-on:
      - self-hosted
    container:
      image: ghcr.io/k1low/runn
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Run Scenario Test
        run: |
          runn run --verbose sample.yml

もしくは、https://github.com/marketplace/actions/run-runn を使ってもいい。

steps:
  -
    uses: actions/checkout@v4
  -
    uses: k2tzumi/runn-action@latest
    with:
      path_pattern: sample.yml

その他の機能

そのほかにも色々機能がありますが、記事が長くなりすぎるので割愛させてください。
気になる方はrunnチュートリアルの19章、21章以降を参考にしてください。

終わりに

まだ、runn自体は v0.123.0 のため、バージョンアップによって破壊的変更が行われる可能性はあります。読者コミニティやリリースノートをみて最新の情報はチェックしておきましょう。

runn開発者会議スレッド
読者コミュニティ|runn クックブック
読者コミュニティ|runnチュートリアル

参考文献

  1. runnチュートリアル
  2. runnとは|runn クックブック
6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?