第4章:runnビルトイン関数編

runnは標準的なexpr-lang関数に加えて、テストシナリオ用の独自ビルトイン関数を提供しています。これらの関数により、テストシナリオの記述が容易になります。

ビルトイン関数の利点

通常のプログラミング言語でテストを書く場合の課題:

  • 差分比較が見にくい
  • テストデータ作成に手間がかかる
  • ファイル操作が複雑
  • 対話的なテストが難しい

runnのビルトイン関数は、これらの課題を解決します。

runnビルトイン関数一覧

以下に主要なビルトイン関数を紹介します。

関数名 用途
urlencode URLパラメータをエンコード urlencode("検索 キーワード")%E6%A4%9C%E7%B4%A2%20%E3%82%AD%E3%83%BC%E3%83%AF%E3%83%BC%E3%83%89
bool 文字列や数値を真偽値に変換 bool("1")truebool("")false
compare レスポンスの厳密な比較 compare(response, expected)
diff 差分の表示 diff(actual, expected)
pick 必要なフィールドの抽出 pick(user, "id", "name")
omit 不要なフィールドの除外 omit(response, "timestamp", "requestId")
merge 複数のオブジェクトを合成 merge(defaults, overrides)
intersect 配列の共通要素を取得 intersect(tagsA, tagsB)
input 対話的な入力 input("APIキーを入力してください:")
secret パスワードの安全な入力 secret("パスワード:")
select 選択肢からの選択 select("環境を選択:", ["dev","prod"], "dev")
basename パスからファイル名を取得 basename("/uploads/image.jpg")image.jpg
time 時刻の統一処理 time("2024-01-01")
faker.* テストデータの自動生成 faker.Name()"田中太郎"
file ファイル内容の読み込み file("./testdata.json")

urlencode関数

日本語や特殊文字を含むパラメータを正しくエンコードするための関数です。

desc: urlencode 関数の使用例
steps:
  urlencode_example:
    dump: |
      urlencode("Hello, World!")

結果:

Hello%2C+World%21

bool関数

APIレスポンスの文字列や数値を真偽値として扱いたい場合に使用します。

desc: bool関数の使用例
vars:
  string_true: "true"
  string_false: "false"
  number_one: 1
  number_zero: 0
  empty_string: ""
steps:
  bool_example:
    desc: 様々な値を真偽値に変換
    bind:
      results:
        string_true: bool(vars.string_true)     # true
        string_false: bool(vars.string_false)   # false
        number_one: bool(vars.number_one)       # true
        number_zero: bool(vars.number_zero)     # false
        empty_string: bool(vars.empty_string)   # false
    test: |
      current.results.string_true == true &&
      current.results.string_false == false &&
      current.results.number_one == true &&
      current.results.number_zero == false &&
      current.results.empty_string == false

変換ルール:

  • 文字列: "true"true"false" や空文字 → false
  • 数値: 1true0false
  • その他: 値が存在すれば true、null や空なら false

compare関数

2つの値を厳密に比較し、差分があればテストを失敗させて詳細を表示します。

desc: compare関数の基本的な使用例

vars:
  # 比較用のデータ
  expected:
    name: "Alice"
    age: 30
    city: "Tokyo"

  actual:
    name: "Alice"
    age: 31
    country: "Japan"

steps:
  compare_example:
    test: |
      // compare関数で差分を検出
      compare(vars.expected, vars.actual)

  compare_with_ignore:
    test: |
      // compare関数で差分を検出
      compare(vars.expected, vars.actual, ['.name'])

結果:

1) examples/runn-builtins/compare_basic.fail.yml 880734e90dcb1377eb0012893664be1268a746da
  Failure/Error: test failed on "compare関数の基本的な使用例".steps.compare_example: condition is not true

  Condition:
    // compare関数で差分を検出
    compare(vars.expected, vars.actual)

    │
    ├── (diff) =>   map[string]any{
    │   -   "age":     float64(30),
    │   +   "age":     float64(31),
    │   -   "city":    string("Tokyo"),
    │   +   "country": string("Japan"),
    │       "name":    string("Alice"),
    │     }
    ├── vars.expected => {"age":30,"city":"Tokyo","name":"Alice"}
    └── vars.actual => {"age":31,"country":"Japan","name":"Alice"}

  Failure step (examples/runn-builtins/compare_basic.fail.yml):
  16   compare_example:
  17     test: |
  18       // compare関数で差分を検出
  19       compare(vars.expected, vars.actual)


1 scenario, 0 skipped, 1 failure

diff関数

差分を色付きで見やすく表示する関数です。

desc: diff関数の使用例

vars:
  # 差分を取るためのテキストデータ
  old_text: "Hello\nWorld\nTest"
  new_text: "Hello\nPlanet\nTest"

steps:
  string_diff_example:
    # テキストの差分
    dump: diff(vars.old_text, vars.new_text)
  json_diff_example:
    # データ構造の差分
    dump: |
      diff(
        {"users": ["Alice", "Bob"]},
        {"users": ["Alice", "Charlie"], "count": 2}
      )

結果:

string(
    "Hello\nWorld\nTest",
    "Hello\nPlanet\nTest",
)
map[string]any{
    "count": float64(2),
    "users": []any{
        string("Alice"),
        string("Bob"),
        string("Charlie"),
    },
}

pick関数

オブジェクトから必要なフィールドだけを抽出する関数です。

desc: pick関数の使用例
vars:
  user:
    id: 1
    name: "Alice"
    email: "alice@example.com"
    password: "secret"
    created_at: "2024-01-01"
steps:
  pick_example:
    dump: |
      // パスワードを除外してユーザー情報を抽出
      pick(vars.user, "id", "name", "email")

結果:

{
  "email": "alice@example.com",
  "id": 1,
  "name": "Alice"
}

omit関数

オブジェクトから不要なフィールドを除外する関数です。タイムスタンプやリクエストIDなど、テストで無視したいフィールドがある場合に便利です。

desc: omit関数の使用例
vars:
  user:
    id: 1
    name: "Alice"
    email: "alice@example.com"
    password: "secret"
    created_at: "2024-01-01"
steps:
  omit_example:
    dump: |
      // パスワードを除外してユーザー情報を抽出
      omit(vars.user, "id", "name", "email")

結果:

{
  "created_at": "2024-01-01",
  "password": "secret"
}

merge関数

複数のオブジェクトを合成する関数です。デフォルト設定に一部だけ上書きしたい場合などに使用します。

desc: merge関数の使用例
vars:
  defaults:
    timeout: 30
    retries: 3
    debug: false
  custom:
    timeout: 60
    verbose: true
steps:
  merge_example:
    dump: |
      // デフォルト設定とカスタム設定をマージ
      merge(vars.defaults, vars.custom)

結果:

{
  "debug": false,
  "retries": 3,
  "timeout": 60,
  "verbose": true
}

intersect関数

配列の共通要素を見つける関数です。

desc: intersect関数の使用例 - 2つの配列の共通要素を取得
vars:
  fruits1: ["apple", "banana", "orange", "grape"]
  fruits2: ["banana", "grape", "melon", "apple"]
  nums1: [1, 2, 3, 4, 5]
  nums2: [3, 4, 5, 6, 7]
  strings1: ["hello", "world", "foo", "bar"]
  strings2: ["foo", "bar", "baz", "hello"]
steps:
  intersect_example:
    dump: |
      {
        "fruits_common": intersect(vars.fruits1, vars.fruits2),
        "numbers_common": intersect(vars.nums1, vars.nums2),
        "strings_common": intersect(vars.strings1, vars.strings2)
      }

結果:

{
  "fruits_common": [
    "apple",
    "banana",
    "grape"
  ],
  "numbers_common": [
    3,
    4,
    5
  ],
  "strings_common": [
    "hello",
    "foo",
    "bar"
  ]
}

input関数

テスト実行時に対話的に値を入力できる関数です。

desc: input関数の使用例
steps:
  -
    bind:
      id: input("Enter your ID", "default")
  -
    dump: id

secret関数

パスワードなどの機密情報を安全に入力するための関数です。入力内容は画面に表示されません。

desc: secret関数の使用例 - パスワードをセキュアに入力
steps:
  -
    bind:
      # パスワードを安全に入力(入力時は表示されない)
      password: secret("Enter your password")
  -
    dump: |
      {
        "message": "Password has been set securely",
        "length": len(password)
      }

select関数

実行時に選択肢から値を選択できる関数です。

desc: select関数を使った対話的な選択
steps:
  select_environment:
    desc: 環境を選択
    # 3つの引数:メッセージ、選択肢リスト、デフォルト値
    dump: select("どの環境にデプロイしますか? (development/staging/production)", ["development", "staging", "production"], "development")

  select_without_default:
    desc: デフォルトなしの選択
    # デフォルト値を空文字列にすると必須選択になる
    dump: select("好きな色を選んでください (red/blue/green/yellow/purple):", ["red", "blue", "green", "yellow", "purple"], "")

basename関数

ファイルパスからファイル名を取得する関数です。

desc: basename関数でファイルパスからファイル名を取得
steps:
  simple_basename:
    desc: 単純なファイルパスからファイル名を取得
    dump: basename("/home/user/documents/report.pdf")

  unix_path:
    desc: Unixパスからファイル名を取得
    dump: basename("/var/log/nginx/access.log")

  windows_path:
    desc: Windowsパスからファイル名を取得  
    dump: basename("C:\\Users\\Documents\\data.xlsx")

  filename_only:
    desc: ファイル名だけの場合
    dump: basename("config.yml")

  trailing_slash:
    desc: 末尾にスラッシュがある場合(最後のディレクトリ名を返す)
    dump: basename("/path/to/directory/")

  empty_path:
    desc: 空のパスの場合(ドットを返す)
    dump: basename("")

  dot_file:
    desc: ドットファイルの場合
    dump: basename("/home/user/.bashrc")

結果:

report.pdf
access.log
C:\Users\Documents\data.xlsx
config.yml
directory
.
.bashrc

time関数

様々な形式の日時文字列を統一的に扱うための関数です。

desc: time関数で文字列や数値を時刻に変換
steps:
  from_string_rfc3339:
    desc: RFC3339形式の文字列を時刻に変換
    dump: time("2024-01-15T10:30:00Z")

  from_string_datetime:
    desc: 日時文字列を時刻に変換
    dump: time("2024-01-15 10:30:00")

  from_string_date:
    desc: 日付文字列を時刻に変換
    dump: time("2024-01-15")

  from_unix_timestamp:
    desc: Unixタイムスタンプ(秒)を時刻に変換
    dump: time(1705320600)

  various_formats:
    desc: 様々なフォーマットの変換
    dump: |
      {
        "slash_date": time("2024/01/15"),
        "us_date": time("January 15, 2024"),
        "with_timezone": time("2024-01-15 10:30:00 +0900")
      }

結果:

"2024-01-15T10:30:00Z"
"2024-01-15T10:30:00Z"
"2024-01-15T00:00:00Z"
"2024-01-15T12:10:00Z"
{
  "slash_date": "2024-01-15T00:00:00Z",
  "us_date": "2024-01-15T00:00:00Z",
  "with_timezone": "2024-01-15T10:30:00+09:00"
}

faker関数群

リアルで多様なテストデータを自動生成する関数群です。

desc: faker関数でテストデータを生成 - 全メソッド網羅版
steps:
  person_data:
    desc: 人物データの生成
    dump: |
      {
        "name": faker.Name(),
        "firstName": faker.FirstName(),
        "lastName": faker.LastName(),
        "email": faker.Email(),
        "username": faker.Username()
      }

  auth_data:
    desc: 認証関連データの生成
    dump: |
      {
        "username": faker.Username(),
        "password_all": faker.Password(true, true, true, true, true, 20),
        "password_lower_only": faker.Password(true, false, false, false, false, 10),
        "password_upper_only": faker.Password(false, true, false, false, false, 10),
        "password_numeric_only": faker.Password(false, false, true, false, false, 10),
        "password_special_only": faker.Password(false, false, false, true, false, 10),
        "password_with_space": faker.Password(true, true, true, false, true, 15)
      }

  misc_data:
    desc: その他の基本データ
    dump: |
      {
        "bool": faker.Bool(),
        "uuid": faker.UUID()
      }

  uuid_variants:
    desc: UUID各バージョンとULID
    dump: |
      {
        "uuidv4": faker.UUIDv4(),
        "uuidv6": faker.UUIDv6(),
        "uuidv7": faker.UUIDv7(),
        "ulid": faker.ULID()
      }

  color_data:
    desc: 色関連データの生成
    dump: |
      {
        "color": faker.Color(),
        "hexColor": faker.HexColor()
      }

  internet_data:
    desc: インターネット関連データの生成
    dump: |
      {
        "url": faker.URL(),
        "domain": faker.Domain(),
        "ipv4": faker.IPv4(),
        "ipv6": faker.IPv6(),
        "httpStatusCode": faker.HTTPStatusCode(),
        "httpMethod": faker.HTTPMethod(),
        "httpVersion": faker.HTTPVersion(),
        "userAgent": faker.UserAgent()
      }

  datetime_data:
    desc: 日時関連データの生成
    dump: |
      {
        "date": faker.Date(),
        "nanoSecond": faker.NanoSecond(),
        "second": faker.Second(),
        "minute": faker.Minute(),
        "hour": faker.Hour(),
        "month": faker.Month(),
        "day": faker.Day(),
        "year": faker.Year()
      }

  emoji_data:
    desc: 絵文字の生成
    dump: |
      {
        "emoji": faker.Emoji()
      }

  number_data:
    desc: 数値データの生成
    dump: |
      {
        "int": faker.Int(),
        "intRange_small": faker.IntRange(1, 10),
        "intRange_medium": faker.IntRange(100, 1000),
        "intRange_large": faker.IntRange(10000, 99999),
        "float": faker.Float(),
        "floatRange_small": faker.FloatRange(0.0, 1.0),
        "floatRange_medium": faker.FloatRange(10.0, 100.0),
        "floatRange_large": faker.FloatRange(1000.0, 10000.0)
      }

  string_data:
    desc: 文字列データの生成
    dump: |
      {
        "digit": faker.Digit(),
        "digitN_5": faker.DigitN(5),
        "digitN_10": faker.DigitN(10),
        "digitN_15": faker.DigitN(15),
        "letter": faker.Letter(),
        "letterN_5": faker.LetterN(5),
        "letterN_10": faker.LetterN(10),
        "letterN_15": faker.LetterN(15),
        "lexify_simple": faker.Lexify("????"),
        "lexify_complex": faker.Lexify("TEST-????-????"),
        "numerify_phone": faker.Numerify("###-###-####"),
        "numerify_code": faker.Numerify("CODE-########")
      }

  edge_cases:
    desc: エッジケースのテスト
    dump: |
      {
        "digitN_0": faker.DigitN(0),
        "digitN_negative": faker.DigitN(-1),
        "letterN_0": faker.LetterN(0),
        "letterN_negative": faker.LetterN(-1),
        "intRange_same": faker.IntRange(42, 42),
        "floatRange_same": faker.FloatRange(3.14, 3.14)
      }

結果:

{
  "email": "kaelynkilback@jakubowski.biz",
  "firstName": "Bobby",
  "lastName": "Wyman",
  "name": "Oscar Doyle",
  "username": "Kirlin6843"
}
{
  "password_all": "D3F4uFJU4aLEln90*7Mz",
  "password_lower_only": "zcwvdfpptm",
  "password_numeric_only": "6714166839",
  "password_special_only": "?_-@#*$_?-",
  "password_upper_only": "SEXVDPPMUY",
  "password_with_space": "45THfz66XE8gk77",
  "username": "Rosenbaum1293"
}
{
  "bool": true,
  "uuid": "3ba59184-6953-4ef4-92c1-87ad948a9316"
}
{
  "ulid": "01K173HV3D0WQ36PEBDSCWSZJH",
  "uuidv4": "f02b8075-cb3f-4b05-aa3c-338195d17496",
  "uuidv6": "01f06b41-a314-62cf-aa8e-000d3a3d89cf",
  "uuidv7": "01984e38-ec6d-77ed-b9c0-7a70be03159e"
}
{
  "color": "FireBrick",
  "hexColor": "#164f05"
}
{
  "domain": "senioropen-source.name",
  "httpMethod": "POST",
  "httpStatusCode": 301,
  "httpVersion": "HTTP/2.0",
  "ipv4": "105.229.235.104",
  "ipv6": "e468:ef69:659f:645d:e073:bc6f:c4a8:d501",
  "url": "https://www.forwardsolutions.net/infrastructures/bricks-and-clicks",
  "userAgent": "Mozilla/5.0 (Windows; U; Windows 98) AppleWebKit/535.44.3 (KHTML, like Gecko) Version/6.0 Safari/535.44.3"
}
{
  "date": "2013-08-10T09:54:41.276197782Z",
  "day": 8,
  "hour": 16,
  "minute": 19,
  "month": 8,
  "nanoSecond": 895525813,
  "second": 37,
  "year": 1963
}
{
  "emoji": "🎪"
}
{
  "float": 9.56449175256422e+307,
  "floatRange_large": 4.577277248245723e+306,
  "floatRange_medium": 1.2302172706489237e+308,
  "floatRange_small": 1.0202585755633835e+306,
  "int": 7482777973562367905,
  "intRange_large": 76995,
  "intRange_medium": 556,
  "intRange_small": 2
}
{
  "digit": "6",
  "digitN_10": "0789564275",
  "digitN_15": "867338934324134",
  "digitN_5": "40047",
  "letter": "H",
  "letterN_10": "QFfSHFPUEK",
  "letterN_15": "zlgthMMykPoyIjl",
  "letterN_5": "awuZJ",
  "lexify_complex": "TEST-RqMn-eTHa",
  "lexify_simple": "xIqU",
  "numerify_code": "CODE-06996186",
  "numerify_phone": "284-628-1882"
}
{
  "digitN_0": "2",
  "digitN_negative": "",
  "floatRange_same": 2.4912506820298563e+307,
  "intRange_same": 42,
  "letterN_0": "A",
  "letterN_negative": ""
}

file関数

ファイルの内容を読み込む関数です。

steps:
  file_example:
    desc: ファイルの内容を読み込む
    bind:
      content: file("./data.txt")
    test: |
      // ファイルの内容を検証
      content != null && len(content) > 0

使用例:

  • 設定ファイルの読み込み
  • テストデータの読み込み
  • テンプレートファイルの読み込み
  • 期待値ファイルとの比較