at kaneshin

Free space for me.

golang の httptest パッケージを使う

この記事は Go Advent Calendar 2016 - Qiita の2日目の記事です。

Golang については書きたいことがたくさんあるので、Go Advent Calendar 2016 その4が出てきても良いのではと思っている次第です。(空いていればいつでも書きます)

さて、今回、この記事では Golang で書かれた Web アプリケーションのリクエストのユニットテストについて解説しようと思います。

github.com

1. Testing HTTP Handler

検証のために、ただ単に "pong" を返却する pingHandler と、URLクエリから値を取得してそのまま返却する echoHandler の2つを定義します。

ー pingHandler

// pingHandler returns just "pong" string.
func pingHandler() func(http.ResponseWriter, *http.Request) {
    return func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("pong"))
    }
}

ー echoHandler

// echoHandler returns a 'msg' query parameter string.
func echoHandler() func(http.ResponseWriter, *http.Request) {
    return func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte(r.URL.Query().Get("msg")))
    }
}

※ ヘッダーやステータスコードの設定は省略しています。

Tests with httptest.Server

本テストコードは こちら です。

実装したHTTPハンドラのテストを実行するために、httptest (net/http/httptest) パッケージを使用してリクエストをサービングします。

ー pingHandler テスト

t.Run("pingHandler", func(t *testing.T) {
    t.Parallel()

    s := httptest.NewServer(http.HandlerFunc(pingHandler()))
    defer s.Close()

    res, err := http.Get(s.URL)
    assert.NoError(t, err)

    defer res.Body.Close()
    body, err := ioutil.ReadAll(res.Body)
    assert.NoError(t, err)
    assert.Equal(t, "pong", string(body))
})

ー echoHandler テスト

t.Run("echoHandler", func(t *testing.T) {
    candidates := []struct {
        query    string
        expected string
    }{
        {"", ""},
        {"foo=bar", ""},
        {"msg=foo", "foo"},
    }
    for _, c := range candidates {
        c := c
        t.Run(c.query, func(t *testing.T) {
            t.Parallel()

            s := httptest.NewServer(http.HandlerFunc(echoHandler()))
            defer s.Close()
            res, err := http.Get(fmt.Sprintf("%v?%v", s.URL, c.query))
            assert.NoError(t, err)

            defer res.Body.Close()
            body, err := ioutil.ReadAll(res.Body)
            assert.NoError(t, err)
            assert.Equal(t, c.expected, string(body))
        })
    }
})

テストのアサートには github.com/stretchr/testify/assert を使用しており、テストの記述については go1.7 以上の仕様となりますので、 Golang におけるサブテストの並行処理実装について | eureka tech blog を是非一読してみてください。

httptest.NewServer とは

上の例のように、既にHTTPハンドラが定義されている場合、 httptest.Server を利用してリクエストをサービングします。

httptest.NewServer(http.Handler) *httptest.Server

httptest.NewServer を呼んで httptest.Server を初期化します。httptest.Server は httptest.NewServer で初期化を行うと、リクエストをサーブする状態の httptest.Server が返却されるため、使用後に Close を行う必要があります。

また、そもそも初期化時にサービングしている httptest.Server が欲しくない場合は

httptest.NewUnstartedServer(http.Handler) *httptest.Server

にて初期化を行います。

httptest.NewServer の使い方

// http.HandleFuncを満たすハンドラの用意
handler := http.HandleFunc(func(w http.ResponseWriter, r *http.Request) {
    // ...
})

// 定義したハンドラをサーブする Server
s := httptest.NewServer(handler)

// s.URL にリクエストURLが設定されている
res, err := http.Get(s.URL)

// ....

// 適切に Close させて終了
s.Close()

httptest.NewServer には http.Handler を満たした変数を渡せば十分です。 http.Handler を渡すこすためには func(w http.ResponseWriter, r *http.Request) のハンドラ関数を http.HandleFunc 型にするだけで満たされます。

handler := http.HandleFunc(func(w http.ResponseWriter, r *http.Request) { ... })

2. Recording Response while Testing

httptest.Server を使用するのは上記で十分ですが、わざわざ検証を行うために httptest.Server を使って検証を行いたくない場合や、何らかの理由で httptest.Server が使用できないが、リクエストをシミュレートしてのユニットテストを行いたい場合は httptest.ResponseRecorder を使用します。

httptest.ResponseRecorder

Responseを記録するための構造体で、httptest (net/http/httptest) にこちらも存在しています。

type ResponseRecorder struct {
    Code      int           // the HTTP response code from WriteHeader
    HeaderMap http.Header   // the HTTP response headers
    Body      *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to
    Flushed   bool
    // contains filtered or unexported fields
}

http.ResponseWriter を満たしているため、HTTPハンドラに渡すことによって結果を書き込むことができます。

Tests with httptest.ResponseRecorder

本テストコードは こちら です。

HTTPハンドラに ResponseRecorder を渡して、その値を検証することになります。

// ResponseRecorder の生成
res := httptest.NewRecorder()

// リクエストの生成
req, err := http.NewRequest(http.MethodGet, "http://example.com/?msg=foo", nil)

用意したリクエストとレスポンスに対してHTTPハンドルします。

// ハンドラに ResponseRecorder と Request を渡して実行する
handler := echoHandler()
handler(res, req)

※ echoHandler は Server で使用したものと同一

// ResponseRecorder に記録された値の検証
assert.Equal(t, "text/plain", res.HeaderMap.Get("Content-Type"))
assert.Equal(t, 200, res.Code)
// Body の検証
body, err := ioutil.ReadAll(res.Body)
assert.NoError(t, err)
assert.Equal(t, c.expected, string(body))

検証には testify/assert を使用しています。httptest.ResponseRecorder には Response 検証に必要な情報が全て格納されているので、それを利用して検証を行います。

おわりに

net/http/httptest パッケージは軽量ですが、テスト時に強力なパッケージになりますので、是非活用してもらえればと思います。

リソース

Yogibo の Traybo という最高のトレー

f:id:laplus-knsn:20161106020413p:plain

膝の上で PC を操作する人に絶対オススメしたい商品。

感想

膝の上に置くことになるので、使い始めは「Traybo が重くてすぐ使わなくなるかも…」と思っていましたが使い心地最高です。 最高な点として、PC が熱を持ってもそれが膝に伝わらないのとトレーの横に iPhone などの小物を置くことができることです。

どこで買うか

日本の Amazon だと倍以上の値段なので、公式のオフラインストアかオンラインストアで購入したほうが良いです。

yogibo.jp

golang で regexp パッケージを使うときに気をつけること

f:id:laplus-knsn:20161015132839p:plain


golang の Regexp は初期化の Compile コストがそこそこありますが、実は正規表現のパターンと対象となる文字列によって、初期化コストが無視できる(パターンと文字列に支配される)くらい遅くなります。よく言われる regexp の初期化コスト問題が無視できると言われても、正直、全く嬉しくないですね。

ただ、コーディング中に regexp パッケージを使わなければいけない場面は出てくるため、なるべくコストの掛からない実装を心がけています。

Compile/MustCompile

初期化コストはなくすため、グローバルに保持するようにします。

定義

var re = regexp.MustCompile("[a-z]{3}")

func main() {
    fmt.Println(re.FindAllString("foobarbazqux", -1))
    // => [foo bar baz qux]
}

グローバルで定義しておけば起動時に実行されるので、Compile でエラーチェックではなく、MustCompile を使用してそもそも起動できなくしています。

var re = regexp.MustCompile("([a-z]{3}")
// => panic: regexp: Compile(`([a-z]{3}`): error parsing regexp: missing closing ): `([a-z]{3}`

func main() {
    fmt.Println(re.FindAllString("foobarbazqux", -1))
}

関数実行で隠蔽してしまうのもアリ。

var finder = func() func(string) []string {
    re := regexp.MustCompile("[a-z]{3}")

    return func(str string) []string {
        return re.FindAllString(str, -1)
    }
}()

func main() {
    fmt.Println(finder("foobarbazqux"))
    // => [foo bar baz qux]
}

Benchmark

regexp を初期化する/しないのベンチマークです。ベンチデータはそこそこ大きいデータを使用しています。

// BenchmarkRegexpMustCompile1-16
// 5000            385760 ns/op          100850 B/op        830 allocs/op
func BenchmarkRegexpMustCompile1(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        re := regexp.MustCompile("[a-z]{3}")
        _ = re.FindAllString(`Lorem ipsum dolor sit amet, consectetur..`)
    }
}

// BenchmarkRegexpMustCompile2-16
// 5000            367423 ns/op           59961 B/op        805 allocs/op
func BenchmarkRegexpMustCompile2(b *testing.B) {
    re := regexp.MustCompile("[a-z]{3}")
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _ = re.FindAllString(`Lorem ipsum dolor sit amet, consectetur..`)
    }
}

func (*Regexp) Copy *Regexp

go1.6 から用意された Copy 関数です。

func (re *Regexp) Copy() *Regexp

コピーの利点は下記の通りです。

Copy returns a new Regexp object copied from re. When using a Regexp in multiple goroutines, giving each goroutine its own copy helps to avoid lock contention.

regexp - The Go Programming Language

要は、 Regexp が複数の goroutine で使われても大丈夫なように goroutine safe に作られているのですが、競合されないようにロックをしているので複数の goroutine で使いまわされることのある Regexp は使用前に Copy せよ、ということになります。

上にあったコードから参考に修正すると、Regexp の関数を実行する前に re := re.Copy() でコピーを取得しています。

var finder = func() func(string) []string {
    re := regexp.MustCompile("[a-z]{3}")

    return func(str string) []string {

        // Copy regexp.
        re := re.Copy()
        return re.FindAllString(str, -1)
    }
}()

func main() {
    list := []string{
        "foo bar baz",
        "kaneshin",
        "shintaro kaneko",
    }

    var wg sync.WaitGroup
    for _, str := range list {
        wg.Add(1)
        str := str
        go func() {
            defer wg.Done()
            fmt.Println(finder(str))
        }()
    }
    wg.Wait()
}

The Go Playground

Benchmark

// !! No Copy
// BenchmarkParallelRegexpMustCompileNoCopy-16
// 5000            375899 ns/op           99689 B/op        817 allocs/op

// BenchmarkParallelRegexpMustCompileCopy-16
// 30000             56602 ns/op           59976 B/op        805 allocs/op
func BenchmarkParallelRegexpMustCompileCopy(b *testing.B) {

    re := regexp.MustCompile("[a-z]{3}")

    b.ResetTimer()
    b.RunParallel(func(pb *testing.PB) {

        // Copy Regexp
        // https://golang.org/src/regexp/all_test.go?s=20028:20077#L718
        re := re.Copy() // ❤️
        for pb.Next() {
            _ = re.FindAllString(`Lorem ipsum dolor sit amet, consectetur..`)
        }
    }
}

Flask を Google App Engine で動作させる


f:id:laplus-knsn:20160905010354p:plain


Google App Engine 上で Python の軽量 Web フレームワークである Flask を使うためのチュートリアルです。

TL;DR

github.com

GAE For Python

とにかく、Flask を App Engine で使うためには App Engine の SDK をインストールしておく必要があります。

インストール

Download the Google App Engine SDK  |  App Engine Documentation  |  Google Cloud Platform

上のリンクにある Google App Engine SDK for Python からインストールすることができます。

Flask on GAE

GitHub - GoogleCloudPlatform/appengine-flask-skeleton: A skeleton for creating Python applications using the Flask framework on App Engine

上のリンクにもありますが、ディレクトリは下のようになります。

app
 |_ app.yaml
 |_ appengine_config.py
 |_ lib/
 |_ main.py

Flask の準備

Flask を App Engine で使うためには Flask をロードしておく必要があります。そのため、ロードできるとうに Flask を lib というディレクトリの中にインストールしておきます。

mkdir lib
pip install -t lib flask

ちなみに、GCPリポジトリでは requirements.txt からインストールするようになっています。

# https://github.com/GoogleCloudPlatform/appengine-flask-skeleton の場合
pip install -r requirements.txt -t lib

この lib ディレクトリを App Engine の実行時にロードするために appengine_config.py へ下記の記述をします。

appengine_config.py

from google.appengine.ext import vendor

# Third-party libraries are stored in "lib", vendoring will make
# sure that they are importable by the application.
vendor.add('lib')

App Engine

App Engine へのリクエストハンドラや静的ファイルへのパス・アプリケーションの設定を app.yaml に記述します。

app.yaml

runtime: python27
api_version: 1
threadsafe: yes

# Handlers define how to route requests to your application.
handlers:
- url: .*
  script: main.app

ルーティング

main.py に Flask をインポートし、簡単なルーティングをします。

main.py

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello():
    """Return a friendly HTTP greeting."""
    return 'Hello World!'

@app.errorhandler(404)
def page_not_found(e):
    """Return a custom 404 error."""
    return 'Sorry, Nothing at this URL.', 404

@app.errorhandler(500)
def application_error(e):
    """Return a custom 500 error."""
    return 'Sorry, unexpected error: {}'.format(e), 500

動作確認

App Engine の SDK にの中にある dev_appserver.py を使用して実行します。

dev_appserver.py .

※dev_appserver.py が実行できない場合、App Engine の SDK が PATH に通っていない可能性が高いです。

実行後、http://localhost:8080 を叩けば "Hello World!" の文字が確認できると思います。

# 動作確認
curl -XGET localhost:8080

デプロイ

デプロイするプロジェクトを GCP の管理画面にて作成し、Project ID を用意し、下記コマンドの [your-project-id] に記述して実行してください。

appcfg.py update -A [your-project-id] -V v1 .

特にエラーがでなければ https://[your-project-id].appspot.com でアクセスすれば "Hello World!" が表示されていると思います。

iOSDC で LT してきました! - Generative Programming in Swift

Generative Programming in Swift // Speaker Deck

Generative Programming とは?

生成的プログラミング(英: Generative programming)とは、ジェネリックなクラス、プロトタイプベース、テンプレート、アスペクト、コード生成などの技法を使ってソースコード作成を自動化し、プログラマの生産性を向上させるプログラミングのスタイルである。ソフトウェアコンポーネントなどのコード再利用の技法とも連携して使用される。

自動プログラミング - Wikipedia

会場には Generative Programming を実施した方がほとんどいないようでしたが、 Generics も Compile 時のことを考えれば C++ のテンプレートと似ていて生成的プログラミングの一種となります。なので、知らず知らずのうちに Generative Programming は大体の Swift 経験者なら経験しています!

今回の LT ではコード生成 (Code Generation) の方をお話したかったので、自分が Swift でクライアントアプリを作るときに使用している ishkawa/APIKit を元にコード生成をするデモコードを前日の夜にちゃちゃっと作ってみました。

github.com

kaneshin/genkit

まだ、デモ程度の実装しかしていないです。

まず、JSON Schema を用意しておき、それを元に genkit を使用します。使用方法は generate.go に書いてありますが

go run /path/to/genkit/cmd/genkit/main.go /path/to/genkit/cmd/genkit/gen.go -path=./Sources -output=apikit_gen.swift ./api.json

の様に使用します。コマンドとしてインストールしててもOKです。

go get github.com/kaneshin/genkit/cmd/genkit
genkit -path=./Sources -output=apikit_gen.swift ./api.json

これを実行すると、 apikit_gen.swift が生成されます。ただ、 Swift には gofmt に相当するものが無いのでコードのインデントが崩れています。

(swiftformat で探してみると案外あるんですね。LT 前に知りたかった…)

github.com

github.com

Swift Code Generators

ちなみに、他にも Generator は存在しています。

github.com

おわりに

別の LT で BaseViewController の話がありましたが、コード生成前提でコードを設計すると、神クラスのような煩雑なコードをつくり上げるようなことがなくなる気がしているので、コード生成はかなりおすすめです。 社内プロジェクトでもコード生成は結構使っているので、実装者はビジネスロジックを考えることに集中することができます。本当におすすめです。


lestrrat さんも builderscon を作っているので、是非参考にすると良いかもです!