runn を Go test 中に呼び出す

runnは、HTTPリクエストのシナリオテストに便利なツールです。CLI として使用することもできますが、Go のテストコード内で runn を使用することで、より柔軟で強力なテストが可能になります。

実際のサーバーに対してリクエストを行おうとすると、正常なリクエストだと判定させるために authentication token を用意したり、 middleware にリクエストが弾かれないような workaround が必要なことがあります。単にリソースサーバーの endpoint に対してシナリオテストを実行したい場合、runn を Go のテストヘルパーとして使うことで、 application 内の router 部分に対してのみテストを実行できます(図)。

runnの公式サイトには多くの情報が掲載されていますが、 機能が豊富な分、具体的な実装を完全な形で見つけるのが少々骨です。実際に動かすことで、テストヘルパーとしての runn の使い方を理解したので、書いておきます。

実装方法

runn を Go のテストヘルパーとして使用する際の重要なポイントは、clientを外部から渡し、runbook内で定義しないことです。

CLI tool として runn の参考文献を勉強していくと、以下のように runner を定義する形で yml ファイルを書く方法に慣れます。

runners:
  req: http://localhost:8080
steps:
- req:
    /resource:
      get:
        headers:
          Accept: application/json
        body: null
  test: |
    current.res.status == 200

しかし、httptestserver などを用いる場合、runner は runn をコード上で呼び出す際に option で渡すため、yml ファイル内の定義は消しておくべきです

ts := httptest.NewServer(NewRouter())
t.Cleanup(func() {
    ts.Close()
})

opts := []runn.Option{
    runn.T(t),
    runn.Runner("req", ts.URL),  # ここで runner を渡す
}
# runners:
#   req: http://localhost:8080  <-- yml ファイル内では定義しない
steps:
- req:
    /resource:
      get:
        headers:
          Accept: application/json
        body: null

実装例

以下が、動作するコードの全体です

main.go

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    mux := NewRouter()

    fmt.Println("Server is running on http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

func NewRouter() http.Handler {
    mux := http.NewServeMux()

    // GETメソッドの処理
    mux.HandleFunc("GET /resource", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "GET request received")
    })

    return mux
}

main_test.go

package main

import (
    "context"
    "net/http/httptest"
    "testing"

    "github.com/k1LoW/runn"
)

func TestSample(t *testing.T) {

    ctx := context.Background()
    ts := httptest.NewServer(NewRouter())
    t.Cleanup(func() {
        ts.Close()
    })

    opts := []runn.Option{
        runn.T(t),
        runn.Runner("req", ts.URL),
    }
    o, err := runn.Load("*.yml", opts...)
    if err != nil {
        t.Fatal(err)
    }
    if err := o.RunN(ctx); err != nil {
        t.Fatal(err)
    }
}

get_resource.yml

desc: resource を GET する
# runners:
#   req: http://localhost:8080
steps:
- req:
    /resource:
      get:
        headers:
          Accept: application/json
        body: null
  test: |
    current.res.status == 200
    && current.res.headers["Content-Length"][0] == "21"
    && current.res.headers["Content-Type"][0] == "text/plain; charset=utf-8"
    && "Date" in current.res.headers
    && current.res.rawBody == "GET request received\n"