日記マン

動画広告プロダクトしてます。Go, Kubernetesが好きです。

HTTPリクエスト・レスポンスレベルのテストユーティリティ

JSON/XML 構造データのアサーション

HTTPのAPIは多くの場合、JSONXMLのフォーマットでレスポンスする。
実際にHTTPリクエストを投げて、期待するレスポンスを検証する際に、
ツリー構造であるJSONXMLに対し特定のフィールドのみ一致するかを検証したい場合が多い。
そんな時は、JSONならば mattn/go-scanXMLならば gopkg.in/xmlpath のパッケージが便利。

package testutils

type ContentType string

const (
    JSON ContentType = "application/json"
    XML  ContentType = "application/xml"
)

func ScanBody(ct ContentType, body io.Reader, p string) (got string, err error) {
    switch ct {
    case JSON:
        got, err = scanJSON(body, p)
    case XML:
        got, err = scanXML(body, p)
    default:
        err = xerrors.Errorf("unimplemented: %s", ct)
        return
    }
    got = strings.TrimRight(got, "\n")
    return
}

func scanJSON(body io.Reader, p string) (string, error) {
    var got string
    if err := scan.ScanJSON(body, p, &got); err != nil {
        return "", err
    }
    return got, nil
}

func scanXML(body io.Reader, p string) (string, error) {
    xpath, err := xmlpath.Compile(p)
    if err != nil {
        return "", err
    }
    node, err := xmlpath.Parse(body)
    if err != nil {
        return "", err
    }
    got, ok := xpath.String(node)
    if !ok {
        return "", xerrors.Errorf("not found node: %s", p)
    }
    return got, nil
}

Code: https://github.com/kazukousen/go-api-utils/blob/master/testutils/scan.go

利用イメージは、

// JSONの場合
got, err := testutils.ScanBody(testutils.JSON, strings.NewReader(`
{
  "foo": [
      {"bar": "baz"},
      {"bar": "faz"}
  ]
}
`), "/foo[1]/bar")

got == "faz" // true

// XMLの場合
got, err := testutils.ScanBody(testutils.XML, strings.NewReader(`
<Foo>
  <Bar>baz</Bar>
  <Bar>faz</Bar>
</Foo>
`), "/Foo/Bar[2]")

got == "faz" // true

こんな感じになる。JSONXMLでツリーパスの仕様が異なるのはちょっと注意。

Code: https://github.com/kazukousen/go-api-utils/blob/master/testutils/scan_test.go

HTTPリクエスト・レスポンスレベルのテスト

httptest パッケージを使うことで実際に http.Handler に対しリクエストを投げてレスポンスを検証することができる。

func request(handler http.Handler, method, path, payload string, wantCode int) (io.Reader, error) {
    srv := httptest.NewServer(handler)
    defer srv.Close()
    path = srv.URL + path

    req, err := http.NewRequest(method, path, strings.NewReader(payload))
    if err != nil {
        return nil, err
    }
    req.Header.Set("Content-Type", "application/json")

    res, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer res.Body.Close()
    if res.StatusCode != wantCode {
        return nil, xerrors.Errorf("not equal StatusCode: got %d, but want %d", res.StatusCode, wantCode)
    }

    buf := new(bytes.Buffer)
    io.Copy(buf, res.Body)

    return buf, nil
}

httptest パッケージを使うことで、実際にサーブしHTTPリクエストをなげることができる。

前述の ScanBody と組み合わせて、
HTTPリクエストを行い返ってきたボディ(JSON/XML)のアサーションを行うユーティリティは以下のように書ける。

func RequestHTTP(handler http.Handler, method, path, payload string, wantCode int, ct ContentType, treePath string, wantBody string) error {
    body, err := request(handler, method, path, payload, wantCode)
    if err != nil {
        return err
    }

    got, err := ScanBody(ct, body, treePath)
    if err != nil {
        return err
    }

    if got != wantBody {
        return xerrors.Errorf("not equal Body:\n\ngot:\n%s\nwant:\n%s\n", got, wantBody)
    }

    return nil
}

Code: https://github.com/kazukousen/go-api-utils/blob/master/testutils/http.go