pkg/errorsの導入時につまづいたところのメモ
最近、既存のプロジェクトにpkg/errorsを導入する機会がありました。使い方のガイドラインが開発者のブログ記事の後半部分にあったので、参考にさせて頂きました。大体問題なく導入できましたが、悩んだ・つまづいたところもあったのでメモしておきます。
そもそもpkg/errorsとは、という説明は省略しますので、必要であればgodocを参照して下さい。
1.関数の引数に渡す関数
関数への引数として関数を渡す時、渡す関数の返すエラーがpkg/errorsで包まれていると、期待通りに動かないことがありました。
例えば、ioutil.ReadAllです。この関数は、引数で与えられたio.Reader
のReadメソッドを、Readメソッドがio.EOF
を返すまで呼び続けます。io.EOF
ではないエラーが返ってきた場合は、エラーを返して終了します。もしpkg/errorsで包まれたio.EOF
が返ってくると、ioutil.ReadAll
はio.EOF
ではないエラーが返ってきたとみなして終了してしまいます。
従って、以下のコードはioutil.ReadAll
がエラーを返して終了します。
package main import ( "fmt" "io" "io/ioutil" "log" "github.com/pkg/errors" ) type OneTimeReader struct { called bool } func (r *OneTimeReader) Read(p []byte) (int, error) { if r.called { return 0, errors.Wrap(io.EOF, "eof") } r.called = true return copy(p, "Hello pkg/errors!"), nil } func main() { r := &OneTimeReader{} b, err := ioutil.ReadAll(r) if err != nil { log.Fatal(err) // ここでエラー終了 } fmt.Printf("%s\n", b) }
対処としては、関数の引数に渡す関数がio.EOF
のような定義済みのエラーを返す必要がある場合はpkg/errorsで包まないようにしました。間違いに気づけば対処は簡単ですが、ちょっと見逃しやすいなーと思いました。僕はテストがFailして気づきました。
2.関数の引数に渡すエラー
1と類似した問題として、関数の引数にエラーを渡す際、エラーがpkg/errorsで包まれていると、期待通りに動かないことがありました。
例えば、os.IsNotExist()のようなエラーを判別する関数にpkg/errorsで包んだエラーを渡すと、trueを返すべき場合でもfalseを返してしまいます。
対処はerr == os.ErrNotExist
のような比較をする場合と同様に、errors.Cause()
でオリジナルのエラーを取り出すだけです。こちらもちょっと気づきにくいケースでした。
3.定義済みのエラーがもつスタックトレース情報
Goの場合、エラーの種別をos.ErrNotExist
のような定義済みのエラーとの比較で判定しているのをよく見かけます。自分たちでも同様の方法でエラー判定をすることがありますが、これをそのままpkg/errorsと組み合わせると、スタックトレース情報があまり意味のないものになってしまいました。
例えば以下のようなエラーを自分で定義しておいたとすると、スタックトレースにはエラーを定義した時点のスタックが残ってしまいます。これだと、実際のエラー発生箇所がスタックトレースから分かりません。
var ErrNotExist = errors.New("file does not exist")
そこで今回は、関数内でエラーを返すときに定義済みのエラーをerrors.Wrap()
で更に包むことにしました。スタックトレースが二重に出てしまいますが。。
エラー判別用の関数を使ってみる
まだ実戦で試せてませんが、より良さそうなのは、こちらの記事で紹介されているエラー判別用の関数を使う方法です。例えば以下のようなコードでは、IsNotExist
がそれにあたります。
type myError struct { msg string notExist bool } func newMyError(msg string, notExist bool) error { return errors.WithStack(&myError{msg, notExist}) } func (e *myError) Error() string { return e.msg } func (e *myError) NotExist() bool { return e.notExist } type notExist interface { NotExist() bool } func IsNotExist(err error) bool { notExistErr, ok := errors.Cause(err).(notExist) return ok && notExistErr.NotExist() }
ポイントとしては以下かと思います。
- エラーを受け取る側は
IsNotExist()
だけ知っていればいい(myError構造体のことや、エラー判別時のerrors.Cause()
のことも知らなくていい) IsNotExist()
はmyError構造体以外も利用できる- 問題が起きた箇所でエラーを生成するので、意味のあるスタックトレース情報を残せる
4.独自のerror構造体
pkg/errorsを利用することでエラーにスタックトレース情報を含めることができますが、場合によっては他の情報も含めたいことがあります。そういう場合は独自のerror構造体を定義すると思いますが、スタックトレース情報はどうやって残すと良いでしょうか。
今回は独自のerror構造体を、生成時にpkg/errorsで包むようにしました。例えば以下のような感じです。
type MyError struct { msg string info string } func NewMyError(msg, info string) error { return errors.WithStack(&MyError{msg, info}) } func (e *MyError) Error() string { return e.msg } func (e *MyError) Info() string { return e.info }
最初は逆に、独自のerror構造体の中にpkg/errorsのerrorを入れようとしていたのですが、fmt.Printf("%+v", err)
で出力する時にスタックトレースが出なくなるので、今の形にしました。
ちなみにこのようなerror構造体についても、上述したエラー判別用の関数のような仕組みにしておけば、より綺麗にInfo等の付随情報を引き出せるかと思います。
感想
既存のプロジェクトへpkg/errorsを導入する際につまづいたところのメモでした。間違いにテストの失敗で気付くことが多く、既存のプロジェクトに入れる場合はテストがちゃんと書かれているのが大前提だなーと思いました。
ここに書いた対処は、これがベストという自信は全くないので、良さそうなやり方がありましたらぜひ教えてください。
Mac OS X El Capitanにrlwrapをソースからインストールする
つまづいたところのメモ。
rlwrapをbrewからインストールしようとしたら、パッケージのダウンロードで失敗していた。
% brew install rlwrap [master] ==> Downloading http://utopia.knoware.nl/~hlub/rlwrap/rlwrap-0.42.tar.gz curl: (7) Failed to connect to utopia.knoware.nl port 80: Operation timed out Error: Failed to download resource "rlwrap" Download failed: http://utopia.knoware.nl/~hlub/rlwrap/rlwrap-0.42.tar.gz
utopia.knoware.nl
の名前解決もできない残念な状況。
仕方ないのでソースから入れることにする。
git clone https://github.com/hanslub42/rlwrap cd rlwrap autoreconf --install ./configure
しかしmakeに失敗する。
% make [994/1376] /Applications/Xcode.app/Contents/Developer/usr/bin/make all-recursive Making all in doc sed -e 's#@DATADIR@#/usr/local/share#' rlwrap.man > rlwrap.1 Making all in src gcc -DHAVE_CONFIG_H -I. -I.. -DDATADIR=\"/usr/local/share\" -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c main.c:813:15: error: use of undeclared identifier 'rl_basic_quote_characters' case 'q': rl_basic_quote_characters = mysavestring(optarg); break; ^ 1 error generated. make[2]: *** [main.o] Error 1 make[1]: *** [all-recursive] Error 1 make: *** [all] Error 2
readlineライブラリにあるはずの関数がないと言っている。調べてみると、Mac OS Xに入っているreadlineライブラリはeditlineというライブラリへのリンクになっているらしい。一方で、rlwrapはGNU readline(が定義している関数)を必要としている。
そういうわけで、GNU readlineをインストールして、そのパスをconfigure時に教えてあげる必要がある。
brew install readline ./configure LDFLAGS="-L/usr/local/opt/readline/lib" CPPFLAGS="-I/usr/local/opt/readline/include" make make install
通った。
参考にしたページ
luciでAppEngine/Goの異常系テストを手軽に書く
こんばんは。アドベントカレンダー Google Cloud Platform(2) 24日目担当の @ks888 です。
昨日の記事に続いて、AppEngine/Goのテストに便利なluci/gaeというライブラリを紹介していきます。昨日は簡単な紹介として、AppEngine/Goのテスト実行時間を短縮する例を出しました。今日は、特に異常系のテストを書くのに役立つ、filter機能とmockのリプレース機能を紹介したいと思います。
filter機能
filterはdatastoreやtaskqueue等のサービスへの操作をプロキシできる機能で、サービスへのアクセス前後で何か処理を行いたいときに役立ちます。luci/gaeには、memcacheをdatastoreの手前に挟むfilterや、サービスへの操作回数をカウントするfilter等が付属しています。もちろん、自分でfilterを作ることもできます。
今回はテストに役立つfilterとして、featureBreakerというfilterを使ってみます。featureBreakerを使うと、サービスの操作に対して強制的に任意のエラーを返させることができます。
前回と同じですが、以下のようなコードを考えます。/user
にPOSTされたデータをDatastoreに書き込んでいます。
package main import ( "encoding/json" "net/http" "github.com/luci/gae/impl/prod" "github.com/luci/gae/service/datastore" "golang.org/x/net/context" "google.golang.org/appengine" ) const userKind = "User" // User ... type User struct { Name string `json:"name"` Email string `json:"email"` } // UserEntity ... type UserEntity struct { User Key *datastore.Key `gae:"$key"` } func init() { http.HandleFunc("/user", userHandler) } func userHandler(w http.ResponseWriter, r *http.Request) { ctx := prod.Use(appengine.NewContext(r), r) switch r.Method { case "POST": handlePostUser(ctx, w, r) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } func handlePostUser(ctx context.Context, w http.ResponseWriter, r *http.Request) { var user User err := json.NewDecoder(r.Body).Decode(&user) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer r.Body.Close() key := datastore.NewKey(ctx, userKind, user.Email, 0, nil) err = datastore.Put(ctx, &UserEntity{user, key}) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) }
これに対して、datastoreへのPutがエラーを返すケースのテストが以下のように書けます。
func Test_HandlePostUser_DatastoreError(t *testing.T) { ctx := memory.Use(context.Background()) ctx, fb := featureBreaker.FilterRDS(ctx, nil) fb.BreakFeatures(nil, "PutMulti") response := httptest.NewRecorder() json := `{"name": "mahiru", "email": "inami@example.com"}` request, _ := http.NewRequest("POST", "/user", strings.NewReader(json)) handlePostUser(ctx, response, request) if response.Code != http.StatusInternalServerError { t.Fatalf("unexpected code: %d", response.Code) } }
3, 4行目でfeatureBreakerを使っています。PutMulti
というメソッドが呼ばれたら、エラーを返すように設定しました。今回はデフォルトのエラーを返す設定にしましたが、任意のエラーを返すこともできます。これだけでエラーケースのテストが書けるのは楽ですね。
ここでちょっとややこしいのが、テストされる側のコードではdatastoreのPut関数を呼んでいるのに、featureBreakerではPutMultiメソッドに対してエラーを返す設定をしています。
これはluci/gaeの設計上致し方ないかなと思っています。というのも、luci/gaeはユーザー向けのインターフェースと内部用のインターフェースをもっており、ユーザー向けのインターフェースの実装中で内部用のインターフェースを呼ぶ、という実装になっています。そして、filterは内部用のインターフェースに対するプロキシとして動作します。
今回のケースでも、Put関数はユーザー向けのインターフェースで、内部ではPutMultiメソッドを呼んでいます。そのため、Put関数にエラーを返させたいときは、PutMultiメソッドに対してfeatureBreakerを設定してあげる必要があります。
mockのリプレース機能
luci/gaeでは、luciが提供するdatastoreやTaskQueueのモックの代わりに、自分で定義したモックを使うこともできます。datastore等に思い通りの挙動をさせることができるので、通常の利用では起きにくい状況、あるいは起きないはずの状況を想定したテストを作成するのに役立ちます。
まずはモックを作ってみます。datastoreのモックとして認識してもらうためは、こちらのインターフェースを実装した構造体が必要です。一つ一つ実装してもいいですが、luci/gaeのdummyパッケージを使うと、独自実装したいメソッドだけ実装すればいいので楽です。
type datastoreMock struct { datastore.RawInterface putMultiCallCount int } func newDatastoreMock() datastore.RawInterface { return &datastoreMock{dummy.Datastore(), 0} } func (m *datastoreMock) PutMulti(keys []*datastore.Key, vals []datastore.PropertyMap, cb datastore.NewKeyCB) error { m.putMultiCallCount++ if m.putMultiCallCount <= 1 { return errors.New("failed to put entities") } return nil }
PutMultiメソッドだけ、自分で実装しています。ここでは、1回目の呼び出しは必ずエラーを返すようにしてみました。リトライのテストとかで使えるかなーと思います。
このモックは、以下のように使います。
func Test_HandlePostUser_Retry(t *testing.T) { ctx := memory.Use(context.Background()) ctx = datastore.SetRaw(ctx, newDatastoreMock()) response := httptest.NewRecorder() json := `{"name": "mahiru", "email": "inami@example.com"}` request, _ := http.NewRequest("POST", "/user", strings.NewReader(json)) handlePostUser(ctx, response, request) if response.Code != http.StatusOK { t.Fatalf("unexpected code: %d", response.Code) } }
3行目のSetRaw()でモックを独自のものに入れ替えています。
かなり雑ですが、テストされる側のコードもPut操作をリトライするように書き換えます。
func handlePostUser(ctx context.Context, w http.ResponseWriter, r *http.Request) { var user User err := json.NewDecoder(r.Body).Decode(&user) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer r.Body.Close() key := datastore.NewKey(ctx, userKind, user.Email, 0, nil) err = datastore.Put(ctx, &UserEntity{user, key}) if err != nil { err = datastore.Put(ctx, &UserEntity{user, key}) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } w.WriteHeader(http.StatusOK) }
テストを実行してみます。
% goapp test ./... ok gaetest/luci 0.038s
ちゃんと動いているようです。独自のモックを作成することで、リトライが発生するケースのテストも簡単に作成できました。
まとめ
luci/gaeのfilter機能とmockのリプレース機能を使ったテストの例を紹介しました。luci/gaeはとにかくドキュメントが足りてない印象があるので、この記事が参考になれば幸いです。