golang の httptest パッケージを使う
この記事は Go Advent Calendar 2016 - Qiita の2日目の記事です。
Golang については書きたいことがたくさんあるので、Go Advent Calendar 2016 その4が出てきても良いのではと思っている次第です。(空いていればいつでも書きます)
さて、今回、この記事では Golang で書かれた Web アプリケーションのリクエストのユニットテストについて解説しようと思います。
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 という最高のトレー
膝の上で PC を操作する人に絶対オススメしたい商品。
感想
膝の上に置くことになるので、使い始めは「Traybo が重くてすぐ使わなくなるかも…」と思っていましたが使い心地最高です。 最高な点として、PC が熱を持ってもそれが膝に伝わらないのとトレーの横に iPhone などの小物を置くことができることです。
どこで買うか
日本の Amazon だと倍以上の値段なので、公式のオフラインストアかオンラインストアで購入したほうが良いです。
golang で regexp パッケージを使うときに気をつけること
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() }
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 で動作させる
Google App Engine 上で Python の軽量 Web フレームワークである Flask を使うためのチュートリアルです。
TL;DR
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
上のリンクにもありますが、ディレクトリは下のようになります。
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)とは、ジェネリックなクラス、プロトタイプベース、テンプレート、アスペクト、コード生成などの技法を使ってソースコード作成を自動化し、プログラマの生産性を向上させるプログラミングのスタイルである。ソフトウェアコンポーネントなどのコード再利用の技法とも連携して使用される。
会場には Generative Programming を実施した方がほとんどいないようでしたが、 Generics も Compile 時のことを考えれば C++ のテンプレートと似ていて生成的プログラミングの一種となります。なので、知らず知らずのうちに Generative Programming は大体の Swift 経験者なら経験しています!
今回の LT ではコード生成 (Code Generation) の方をお話したかったので、自分が Swift でクライアントアプリを作るときに使用している ishkawa/APIKit を元にコード生成をするデモコードを前日の夜にちゃちゃっと作ってみました。
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 前に知りたかった…)
Swift Code Generators
ちなみに、他にも Generator は存在しています。
おわりに
別の LT で BaseViewController の話がありましたが、コード生成前提でコードを設計すると、神クラスのような煩雑なコードをつくり上げるようなことがなくなる気がしているので、コード生成はかなりおすすめです。 社内プロジェクトでもコード生成は結構使っているので、実装者はビジネスロジックを考えることに集中することができます。本当におすすめです。
buildersconではGo Python PerlのコードをJSON Schemaから作ってる。便利 #iosdc
— Daisuke Maki (@lestrrat) August 20, 2016
lestrrat さんも builderscon を作っているので、是非参考にすると良いかもです!