はじめに
この記事は、さくらインターネット 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
を作成します。
desc: hello
steps:
-
dump: "'Hello world!'"
そして、hello.yml
を実行すると、Hello world! が出力できます。
stepsは必要項目ですが、-
とすることで省略できます。
$ runn run hello.yml
Hello world!
.
1 scenario, 0 skipped, 0 failure
GETリクエスト
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}}
のように指定する
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
指定した変数をマスクすることができる。
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になる。s や ms, m の単位を明示して指定することも可能。
if
if
が true の時だけrunbookが実行される。
if: included
vars:
foo: "foooo"
steps:
step1:
test: |
vars.foo == "foooo"
step2:
test: |
vars.foo == "foooo"
included
を指定すると、異なるrunbookからコールされた時のみ included
が true になり、実行される。
$ runn run sample.yml --verbose
=== [No Description] (sample.yml)
--- (step1) ... skip
--- (step2) ... skip
1 scenario, 1 skipped, 0 failures
スキップされたので、1 skipped
となる。
skipTest
skipTest
を true にするとテストが全てスキップされる。
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を指定できる。
includeとの違い : https://zenn.dev/link/comments/5b92ba64645eb6
vars:
foo: "hello"
steps:
step1:
exec:
command: |
echo "{{ vars.foo }} world!"
bind:
greeting: current.stdout
sample.yml
のneedsで sample2.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番目とすると、current
はsteps["i番目のstep名"]
のエイリアスとなっている。ゆえに、上記の場合はcurrent.res.status
とsteps["getUser"].res.statusは同値となる。
また、previous
はsteps[i-1番目のstep名]
のエイリアスとなる。ちなみに、step名がない場合は、steps[0], steps[1]・・・のようにindexが振り分けられる。
参考: ステップに名前を付ける|runnチュートリアル
include
includeを使うとRunbookを再利用することができる。
steps:
-
include:
path: sample2.yml
vars:
foo: "foooo"
test: current.greeting == "foooo world!"
再利用するランブックにはif: included
をつけることで、再利用専用にすることができる。
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 }}"
ゆえに、親のfoo
がsample.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だけ変数にするのではなく、まとめて変数にして外部から注入してあげてるのがおすすめです。
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
の値はあえて変数で定義しています。
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で動かす
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章以降を参考にしてください。
- OpenAPIのテスト
- カバレッジの取得 : 開発者の k1LoW さんはGoのカバレッジツールoctocovの開発者でもあるので連携が可能です
- DBとの連携
- CDPを使ってブラウザ操作してみる
終わりに
まだ、runn自体は v0.123.0
のため、バージョンアップによって破壊的変更が行われる可能性はあります。読者コミニティやリリースノートをみて最新の情報はチェックしておきましょう。
runn開発者会議スレッド
読者コミュニティ|runn クックブック
読者コミュニティ|runnチュートリアル