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

## はじめに

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

## runnとは？

https://github.com/k1LoW/runn

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

## runnの特徴

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

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

## インストール

### homebrew tap

```shell
brew install k1LoW/tap/runn
```

### Go

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

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

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

https://github.com/k1LoW/runn?tab=readme-ov-file#install

## runnのオプション

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

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

### バージョン確認

```shell_session
$ runn -v
runn version 0.123.0
```

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

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

```shell_session
$ 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 クックブック](https://zenn.dev/k1low/books/runn-cookbook/viewer/new-grpcurl))

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

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

### プロファイリング

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

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

```shell_session
$ 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` 。

```shell_session
runn completion [your shell]
```

### 負荷テスト

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

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

```shell_session
$ 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` でカバレッジを出力できる。

### シナリオ出力

```shell_session
$ 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`を作成します。

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

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

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

1 scenario, 0 skipped, 0 failure
```

### GETリクエスト

```yml: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
```

```shell_session:実行結果
$ 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](https://github.com/k1LoW/runn/blob/11ee56c1d4e6ab1d8866467ade0bb4939d95b702/runbook.go#L41-L61)

### runners

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

```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
```

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

```yml
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と証明書](https://github.com/k1LoW/runn/tree/main?tab=readme-ov-file#custom-ca-and-certificates) や [X-Runn-Traceトレース用のHTTPリクエストにヘッダーを追加する](https://github.com/k1LoW/runn/tree/main?tab=readme-ov-file#add-x-runn-trace-header-to-http-request-for-tracing)、[gRPC ランナー: gRPC リクエストを実行する](https://github.com/k1LoW/runn/tree/main?tab=readme-ov-file#grpc-runner-do-grpc-request)などができる。

発展的な内容として、runnersに`cc: chrome://new`を設定することでCDPを使って、**selenium** みたいにブラウザを操作できるようになります。(参考 : [CDPを使ってブラウザ操作してみる｜runnチュートリアル](https://zenn.dev/katzumi/books/runn-tutorial/viewer/cdp-runner))

### vars

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

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

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

```yaml:sample.yml
desc: hello
vars:
  foo: "foooo"
steps:
  cmd:
    exec:
      command: echo "{{ vars.foo }}"
      liveOutput: true
```

```shell_session:実行結果
$ runn run --scopes run:exec sample.yml
foooo
.

1 scenario, 0 skipped, 0 failures
```

### secrets

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

```yaml:sample.yml
vars:
  foo: "foooo"
secrets:
  - vars.foo
steps:
  -
    dump: vars.foo
```

```shell_session:実行結果
$ runn run sample.yml
*****
.

1 scenario, 0 skipped, 0 failures
```

### labels

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

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

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

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

or

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

### hostRules

名前解決ができる。

```yml
hostRules:
  localhost: 127.0.0.2:8000
```

### debug

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

```yml
debug: true
steps:
    # ・・・・
```

### interval

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

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

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

### if

`if` が **true** の時だけrunbookが実行される。

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

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

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


1 scenario, 1 skipped, 0 failures
```

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

### skipTest

`skipTest` を **true**  にするとテストが全てスキップされる。

```yml
skipTest: true
vars:
  foo: "foooo"
steps:
  step1:
    test: |
      vars.foo == "foooo"
  step2:
    test: |
      vars.foo == "foooo"
```

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


1 scenario, 0 skipped, 0 failures
```

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

### force

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

### trace

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

参考 : [アクセスログからシナリオを特定する｜runnチュートリアル](https://zenn.dev/katzumi/books/runn-tutorial/viewer/trace)

### loop

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

```yml
loop: 100
```

or

```yml
loop:
  count: 100
```

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

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

### concurrency

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

### needs

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

:::note info
includeとの違い : https://zenn.dev/link/comments/5b92ba64645eb6
:::

[ランブック間に依存関係を設定する｜runn クックブック](https://zenn.dev/k1low/books/runn-cookbook/viewer/needs#%5B%E3%82%B3%E3%83%A9%E3%83%A0%5D-needs%3A-%E3%81%AE%E7%9D%80%E6%83%B3%E3%81%A8%E5%AE%9F%E8%A3%85)

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

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

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

実行すると、

```shell_session:実行結果
$ 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をかく。

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

## stepごとの設定値

stepごとに設定できる項目

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

### dump

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

```yml
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のとき出力時に<br />シークレットをマスクする  | false  |

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

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

### exec

コマンドを実行できる。

```yml
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する場合は、この値を使用する。

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

### bind

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

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

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

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

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

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

:::

### test

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

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

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

```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
      && current.res.body.id == 1
      && current.res.body.name == "Leanne Graham"
```

:::note info
レスポンスは`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チュートリアル](https://zenn.dev/katzumi/books/runn-tutorial/viewer/ordered-map)
:::

### include

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

```yml: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
```

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

```shell_session:実行結果
$ 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のデフォルト値になる。

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

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

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

## 環境変数の読み込み

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

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

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

```yml
vars:
  endpoint: ${BASE_URL}
```

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

## ビルトイン関数

### bool()

bool値にキャストする。

```yml
steps:
    - dump: bool(1) # true
```

### urlencode()

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

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

### compare()

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

```yml
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()と同じ。

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

steps:
    - dump: diff(vars.groupA, vars.groupB)
```

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

### pick()

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

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

steps:
    - dump: pick(vars, "dog",　"cat") 
```

```json:実行結果
{
  "cat": "meow",
  "dog": "bowwow"
}
```

### omit()

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

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

steps:
    - dump: omit(vars, "dog","cat") 
```

```json:実行結果
{
  "chicken": "cock-a-doodle-doo",
  "cow": "moo"
}
```

### merge()

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

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

steps:
    - dump: merge(vars.groupA, vars.groupB)
```

```txt:実行結果
$ runn run sample.yml
{
  "cat": "meow",
  "chicken": "cock-a-doodle-doo",
  "cow": "moo",
  "dog": "bowwow"
}
```

### input()

標準入力。

```yml
steps:
    - dump: input("数字を入力してください", "") 
```

```:実行結果
数字を入力してください: 3
3
```

### secret()

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

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

### select()

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

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

```txt:実行結果
a,b,cの中から選んでください。 [a]: c
c
```

### basename()

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

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

### time()

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

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

### faker.*

https://pkg.go.dev/github.com/k1LoW/runn/builtin#Faker

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

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

## APIシナリオテスト実践編

### POSTリクエストの実装

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

```yml: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`の値はあえて変数で定義しています。

```yml: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](https://pkg.go.dev/text/template)形式のファイルにしなければなりません。Go Templateにするには、拡張子を`.json.template`にする必要があるそうです。(参考 : [JSONファイルにパラメータ埋め込みをしてみる｜runnチュートリアル](https://zenn.dev/katzumi/books/runn-tutorial/viewer/template))

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

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

:::note warn
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 を使ってもいい。

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

## その他の機能

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

* [OpenAPIのテスト](https://zenn.dev/katzumi/books/runn-tutorial/viewer/openapi)
* [カバレッジの取得](https://zenn.dev/katzumi/books/runn-tutorial/viewer/coverage#octocov%E3%81%AE%E8%A8%AD%E5%AE%9A) : 開発者の k1LoW さんはGoのカバレッジツール[octocov](https://github.com/k1LoW/octocov)の開発者でもあるので連携が可能です
* [DBとの連携](https://zenn.dev/katzumi/books/runn-tutorial/viewer/db-runner)
* [CDPを使ってブラウザ操作してみる](https://zenn.dev/katzumi/books/runn-tutorial/viewer/cdp-runner)

## 終わりに

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

[runn開発者会議スレッド](https://zenn.dev/katzumi/scraps/f00a2d3e177b77)
[読者コミュニティ｜runn クックブック](https://zenn.dev/k1low/scraps/cf7c7ef06f9051)
[読者コミュニティ｜runnチュートリアル](https://zenn.dev/katzumi/scraps/a1671ad5e2a03e)

## 参考文献

1. [runnチュートリアル](https://zenn.dev/katzumi/books/runn-tutorial?__hstc=53729015.329bd63bc35f0b533cdd375d069d9e94.1725942874120.1733278753839.1733707112600.17&__hssc=53729015.1.1733707112600&__hsfp=2614066815)
1. [runnとは｜runn クックブック](https://zenn.dev/k1low/books/runn-cookbook/viewer/about)