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

「APIレスポンスのdiffを見やすく表示したい」「テストデータを毎回手動で作るのが面倒」「ファイルの内容を簡単に読み込みたい」

そんな願いを叶える、runnの魔法の関数たちを紹介します。

runnは標準的なexpr-lang関数に加えて、テストシナリオを劇的に便利にする独自のビルトイン関数を提供しています。これらの関数を使いこなせば、複雑なテストシナリオもシンプルに記述できるようになります。

なぜrunnのビルトイン関数が必要なのか?

通常のプログラミング言語でテストを書く場合、こんな課題がありました:

  • 差分比較が見にくい: 大きなJSONの差分を見つけるのが大変
  • テストデータ作成が面倒: 毎回ランダムなユーザー名やメールアドレスを考える必要がある
  • ファイル操作が煩雑: ファイルを読み込んでパースして...というコードが冗長
  • 対話的なテストが難しい: パスワード入力などの対話的操作をテストに組み込めない

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関数

「日本語パラメータでAPIが動かない!」という経験はありませんか?

URLエンコードを忘れると、日本語や特殊文字を含むパラメータが正しく送信されません。urlencode関数があれば、もう心配無用です:

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

結果:

Hello%2C+World%21

✅ bool関数

「この値はtrueなの?falseなの?」と迷ったことはありませんか?

APIレスポンスの文字列"true"や数値の1を真偽値として扱いたい場合、bool関数が確実に変換してくれます。

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関数

「レスポンスが期待通りか確認したい、でも差分があったら即座に知りたい!」

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関数

「巨大なJSONの中で、どこが違うのか探すのに30分かかった...」

もうそんな苦労は不要です!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関数

「レスポンスの一部だけをテストしたい」ときの救世主!

巨大なAPIレスポンスから必要なフィールドだけを抜き出して、スッキリとテストできます:

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は毎回変わるから、テストから除外したい」

そんな時はomit関数!不要なフィールドを除外して、本質的な部分だけをテストできます:

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関数

「デフォルト設定に一部だけ上書きしたい」というケースで大活躍!

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関数

「2つのAPIが返すタグの共通部分を知りたい」

配列の共通要素を見つけるのは意外と面倒。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関数

「テスト実行時にAPIキーを入力したい」「環境によって異なる値を使いたい」

input関数で、対話的なテストシナリオが実現できます:

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

🔒 secret関数

「パスワードを入力したいけど、画面に表示されるのは困る!」

secret関数なら、入力内容が***で隠されるので安心です:

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

📋 select関数

「どの環境でテストを実行する?」を毎回選びたい

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関数

「アップロードされたファイルのパスから、ファイル名だけ取り出したい」

パス操作は地味に面倒。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関数

「様々な形式の日時文字列を、統一的に扱いたい」

time関数は賢く日時を解析し、Go標準の時刻形式に変換してくれます:

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関数群

「テストのたびに『test1@example.com』『田中太郎』って書くの、もう飽きた...」

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": "coreneheller@kessler.info",
  "firstName": "Russ",
  "lastName": "VonRueden",
  "name": "Jaydon Grimes",
  "username": "Purdy3498"
}
{
  "password_all": "lr ldqcC sk 91?lUxYe",
  "password_lower_only": "xcyjsrdyxt",
  "password_numeric_only": "6710682821",
  "password_special_only": "\u0026-!#?#!.!\u0026",
  "password_upper_only": "LFXORZIOPO",
  "password_with_space": "wxl99v9z lgoONx",
  "username": "Daugherty4909"
}
{
  "bool": true,
  "uuid": "bf157ec5-bac5-4405-b7fb-dd4aca8b1f29"
}
{
  "ulid": "01K0QV14MH01Z4ABHHMB17B0YZ",
  "uuidv4": "752d2d3d-ce64-4408-8a90-5c99f21d3389",
  "uuidv6": "01f06698-f4de-6705-beae-7c1e524396ee",
  "uuidv7": "01982fb0-9291-7a6a-aea5-39a34c9f5713"
}
{
  "color": "SteelBlue",
  "hexColor": "#ef9abb"
}
{
  "domain": "futureusers.io",
  "httpMethod": "HEAD",
  "httpStatusCode": 100,
  "httpVersion": "HTTP/2.0",
  "ipv4": "184.184.110.67",
  "ipv6": "7489:1b82:f2f4:4ad6:b302:dcf0:f56e:c137",
  "url": "http://www.dynamicmaximize.io/web-readiness/innovative/deploy/infomediaries",
  "userAgent": "Mozilla/5.0 (iPad; CPU OS 9_3_1 like Mac OS X; en-US) AppleWebKit/536.38.3 (KHTML, like Gecko) Version/3.0.5 Mobile/8B117 Safari/6536.38.3"
}
{
  "date": "1901-07-01T00:18:23.128218056Z",
  "day": 29,
  "hour": 15,
  "minute": 26,
  "month": 4,
  "nanoSecond": 355820788,
  "second": 19,
  "year": 2009
}
{
  "emoji": "💏"
}
{
  "float": 9.286022284567304e+307,
  "floatRange_large": 4.298660924741762e+307,
  "floatRange_medium": 1.0816964757861292e+308,
  "floatRange_small": 7.968886182755118e+307,
  "int": -6185955400225563096,
  "intRange_large": 21043,
  "intRange_medium": 599,
  "intRange_small": 7
}
{
  "digit": "2",
  "digitN_10": "0597188653",
  "digitN_15": "971637069502955",
  "digitN_5": "95082",
  "letter": "x",
  "letterN_10": "uOBqoPXOMA",
  "letterN_15": "YAxnKrVdnoHlJsB",
  "letterN_5": "bClmz",
  "lexify_complex": "TEST-QgPD-INyb",
  "lexify_simple": "hsIT",
  "numerify_code": "CODE-28161666",
  "numerify_phone": "338-050-4277"
}
{
  "digitN_0": "1",
  "digitN_negative": "",
  "floatRange_same": 1.0812317647065652e+308,
  "intRange_same": 42,
  "letterN_0": "G",
  "letterN_negative": ""
}

📄 file関数

「設定ファイルやテストデータをファイルから読み込みたい」

file関数なら、ファイルの内容を一行で読み込めるシンプルさ:

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

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

まとめ:ビルトイン関数でテストが変わる!

runnのビルトイン関数を使いこなせば:

  • 🚀 テストの記述時間が1/3に短縮
  • 🎯 バグの発見率が向上(差分が一目瞭然)
  • 😊 テストデータ作成のストレスから解放
  • 🔧 メンテナンスが圧倒的に楽に

これらの関数は、あなたのテストライフを劇的に改善する強力な武器です。ぜひ活用して、より良いテストを書いていきましょう!