goでテスト用にhttpの通信を失敗させたかったのでその方法のメモ
たまにエラー時のハンドリング部分のテストが書きたくなることがありますね。例えば、通信の記録のような操作を書いているときなどに。 こういうときに人為的にエラーを発生させる方法を調べたのでメモをしておこうと思います。
タイムアウト分待つ
まずはじめに考えることはタイムアウトかもしれません。これは *http.Client
にタイムアウトを指定できるのでそれ以上の時間を待ってあげれば良いと言うようなコードです。
=== RUN TestTimeout x_test.go:14: for test -- timeout forcely 200ms --- PASS: TestTimeout (0.11s)
コードは以下です。タイムアウト用のエラーが正しく返ってきたかのテストがめんどくさいですね。実はこれを書いているときにerrors.Asがインターフェイスも取れたことに気づきました。考えてみればそうですね。
package expecterror import ( "errors" "net/http" "net/http/httptest" "testing" "time" ) func TestTimeout(t *testing.T) { timeout := 100 * time.Millisecond ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Logf("for test -- timeout forcely %v", 2*timeout) time.Sleep(timeout) })) defer ts.Close() client := &http.Client{Timeout: timeout} _, err := client.Get(ts.URL) if err == nil { t.Errorf("error is expected but nil") } // context.DeadlineExceeded あたりになってくれれば嬉しいのだけれど。。 // &url.Error{Op:"Get", URL:"http://127.0.0.1:59233", Err:(*http.httpError)(0xc0001b2040)} var x interface{ Timeout() bool } if ok := errors.As(err, &x); !(ok && x.Timeout()) { t.Errorf("timeout is expected but return error is %[1]T, %#+[1]v", err) } }
とはいえ、特定の時間分だけ待つと言うテストは微妙な感じですね。可能ならテスト中のtime.Sleep()は禁止にしたいところです。
無理やりコネクションを切断する
次は、貼ったコネクションを無理やり切断する方法です。これにはhttp.Hijackerというインターフェイスを使います。もしかしたら、存在を知らない人は結構いるかも知れません。
=== RUN TestCloseConnection x_test.go:17: for test -- close connection --- PASS: TestCloseConnection (0.00s)
このようなコードになります。コネクションをぶった切っている感がわかりやすいコードですね。
package expecterror import ( "errors" "io" "net/http" "net/http/httptest" "testing" ) func TestCloseConnection(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, _, err := w.(http.Hijacker).Hijack() if err != nil { t.Fatal(err) } t.Log("for test -- close connection") c.Close() })) defer ts.Close() _, err := http.Get(ts.URL) if err == nil { t.Errorf("error is expected but nil") } if !errors.Is(err, io.EOF) { t.Errorf("EOF is expected but return error is %[1]T, %+[1]v", err) } }
⚠ httptestのRecorderの方は使えない
webAPIのテストをする際に利用するもう一つの方法としてRecorderを使う方法がありますが、こちらは使えません。こちらはユニットテスト用のヘルパーなのでハンドラー関数をそのまま呼ぶだけですし、通信はしていないので当たり前といえば当たり前ですね。まぁ一応。メモなので。
// WARNING: これは動かない func TestErrorWithRecorder(t *testing.T) { rec := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, _, err := w.(http.Hijacker).Hijack() if err != nil { t.Fatal(err) } t.Log("for test -- close connection") c.Close() }) handler(rec, httptest.NewRequest("GET", "http://example.net", nil)) }
panicします。
panic: interface conversion: *httptest.ResponseRecorder is not http.Hijacker: missing method Hijack [recovered] panic: interface conversion: *httptest.ResponseRecorder is not http.Hijacker: missing method Hijack
常に失敗を返すtransportを挟む
今までエラーを発生させてきたのは通信先のリモート側(httptest.Serverで動いているハンドラー側)でしたが、これだけだとサーバー側の処理ではなくクライアント側の処理を書いているときに少し困ってしまうかもしれませんね。そのような場合にはClientがTransportというフィールドにhttp.RoundTripperが取れるのでこれが使えそうです。
これはgoでhttp(https)の通信が行われてるときに、実際の通信の処理自体はDefaultTransportというstructで行われているのですが、その部分を差し替えるということです。ちなみにtransportをラップするtransportを書いてあげることでいわゆるミドルウェアのようなものを作ることは結構それなりに知られた常套手段だったりします。
=== RUN TestErrorTransport x_test.go:19: error forcely *errors.errorString --- PASS: TestErrorTransport (0.00s)
コードは以下です。インターフェイスの実装になるのでstructとメソッドの2つが必要になるのは少し面倒ですね。
package expecterror import ( "errors" "fmt" "net/http" "testing" "time" ) type errorTransport struct { T *testing.T genErr func() error } func (t *errorTransport) RoundTrip(req *http.Request) (*http.Response, error) { err := t.genErr() t.T.Logf("error forcely %T", err) return nil, err } func TestErrorTransport(t *testing.T) { this := fmt.Errorf("*THIS*") transport := &errorTransport{T: t, genErr: func() error { return this }} client := &http.Client{Timeout: 100 * time.Millisecond, Transport: transport} _, err := client.Get("http://example.net.dummy") // テキトーなURL if err == nil { t.Errorf("error is expected but nil") } if !errors.Is(err, this) { t.Errorf("%#[2]v is expected but return error is %[1]T, %+[1]v", err, this) } }
おわりに
通信を失敗させる部分の処理が時々欲しくなるのですが、それが時々だからこそ調べる必要がでてきてめんどくさく感じたりします。そんなわけで調べたついでにメモしてみたのでした。 ふとその場で思いつくような実装はこれくらいでしたが、その他手軽な良い方法があれば知りたいところです。
追記
これを試したときのgoのversionは以下です。
$ go version go version go1.15.1 darwin/amd64
gist
goでflexibleな実装の選択のためのちょっとしたパターンのメモ
前回の記事でxormのコードを読んでいたときに、flexibleに実装を選択するためのパターンがあったことを思い出したのでそのメモ。
flexibleな実装の選択?
実装の選択と言っても、DI的な話ではない。デフォルト実装と拡張の実装の間を自由に行き来できるような実装のパターンのこと。もう少し具体的に言うと、デフォルト実装であるBaseとその拡張の実装であるXの間で以下のような関係が成り立つもののこと。
- 拡張実装(X)からデフォルト実装(Base)からの任意の処理が呼べる
- デフォルト実装(Base)から拡張実装(X)の任意の処理が呼べる
- 拡張実装(X)はデフォルト実装(Base)の定義を上書きできる
もう少しわかりやすい例で言えば、ORM的なライブラリを作っていたとして、mysqlやpostgresのような方言部分が拡張実装。そしてインターフェイスとしてDialectを持っているというような感じ。そしてbase側から拡張側で実装した処理も呼べるようにするにはどうしたら良いか?というような話。
要件
ここからは、どのような挙動のものが欲しいかを説明するもう少し具体的な例を紹介することにする
まず、どのような機能を提供するかのインターフェイスを定義する。Managerという名前を付けてみる1。Managerは以下のようなもの。このようなインターフェイスがあったとして、
type Manager interface { CreateTable(name string) DropTable(name string) RefreshTable(name string) // Drop + Create Prefix() string TypeOf(ob interface{}) string }
これのデフォルト実装部分を担ったBaseというstructがあり2、その拡張としてXというstructが存在するということにする。
最終的には、以下のmain部分のように、RefrshTable()とTypeOf()が使えるようになることを目指す。ここでRefreshTable()はCreateTable()とDropTable()を呼び出す実装。
func main() { x := NewXManager("foo") x.RefreshTable("Target") // Manager.RefreshTable() // xで拡張されたTypeOfが呼べる fmt.Println("----------------------------------------") fmt.Println("10 is", x.TypeOf(10)) fmt.Println("foo is", x.TypeOf("foo")) fmt.Println("0.1234 is", x.TypeOf(0.1234)) }
このような振る舞いをするコードを例として実装していく。
実装例
ここからは具体的な実装例の話。以下の3つの対応ができれば良いという話だった。
- 拡張実装(X)からデフォルト実装(Base)からの任意の処理が呼べる
- デフォルト実装(Base)から拡張実装(X)の任意の処理が呼べる
- 拡張実装(X)はデフォルト実装(Base)の定義を上書きできる
拡張実装(X)からデフォルト実装(Base)からの任意の処理が呼べる
これは埋め込みを使えば完成する。したがってXの定義が以下の様になっていれば良い。 ここで、CreateTable()はデフォルト実装で定義したもの。埋め込みなので直接呼べる。
type X struct { Base } func (x *X) RefreshTable(name string) { fmt.Println("refresh..") x.DropTable(name) // こちらはXで定義した実装 x.CreateTable(name) // こちらはBaseで定義したdefault実装 } func (x *X) DropTable(name string) { fmt.Printf("%-5s\t%-10s\t%s.%s\n", "in X", "drop table", x.Prefix(), name) }
デフォルト実装(Base)から拡張実装(X)の任意の処理が呼べる
ここがパターンと呼んで良い肝となる部分かもしれない。利用する機能をインターフェイス3として定義し、Baseがこのインターフェイスをフィールドとして保持するような構造にしてあげる。そして拡張実装のstructがこのインターフェイスを実装する。
このフィールドを通して呼ぶことで、デフォルト実装側から拡張実装側の処理が呼べるようになる。
type Base struct { manager Manager } func (b *Base) CreateTable(name string) { // BaseからXのPrefix()が呼べる fmt.Printf("%-5s\t%-10s\t%s.%s\n", "in Base", "create table", b.manager.Prefix(), name) } type X struct { Base prefix string // prefixを追加した } func (x *X) Prefix() string { return x.prefix }
実はこの種の実装パターンを一度記事にした事があった。
リンク先の記事はフィールドの型が特定のstructだったがこちらの定義ではインターフェイスになっている。そういう意味ではリンク先の記事の応用と言えるかもしれない。
拡張実装(X)はデフォルト実装(Base)の定義を上書きできる
こちら素直。埋め込んだ型(Base)と同名のメソッドを定義すれば良いだけ。
func (b *Base) TypeOf(ob interface{}) string { return "Unknown" } func (x *X) TypeOf(ob interface{}) string { // Baseで定義したTypeOfを上書きできる(分岐を増やせる) switch reflect.TypeOf(ob).Kind() { case reflect.Int: return "Integer" case reflect.String: return "String" default: return x.Base.TypeOf(ob) } }
はい。
実装例
全部つなげたコード例は以下の様になる。
package main import ( "fmt" "reflect" ) type Manager interface { CreateTable(name string) DropTable(name string) RefreshTable(name string) Prefix() string TypeOf(ob interface{}) string } type Base struct { manager Manager } func (b *Base) CreateTable(name string) { // BaseからXのPrefix()が呼べる fmt.Printf("%-5s\t%-10s\t%s.%s\n", "in Base", "create table", b.manager.Prefix(), name) } func (b *Base) TypeOf(ob interface{}) string { return "Unknown" } type X struct { Base prefix string } func (x *X) TypeOf(ob interface{}) string { // Baseで定義したTypeOfを上書きできる(分岐を増やせる) switch reflect.TypeOf(ob).Kind() { case reflect.Int: return "Integer" case reflect.String: return "String" default: return x.Base.TypeOf(ob) } } func (x *X) RefreshTable(name string) { fmt.Println("refresh..") x.DropTable(name) // こちらはXで定義した実装 x.CreateTable(name) // こちらはBaseで定義したdefault実装 } func (x *X) DropTable(name string) { fmt.Printf("%-5s\t%-10s\t%s.%s\n", "in X", "drop table", x.Prefix(), name) } func (x *X) Prefix() string { return x.prefix } func NewXManager(prefix string) Manager { x := &X{prefix: prefix} x.Base.manager = x return x } func main() { x := NewXManager("foo") x.RefreshTable("Target") // Manager.RefreshTable() // xで拡張されたTypeOfが呼べる fmt.Println("----------------------------------------") fmt.Println("10 is", x.TypeOf(10)) fmt.Println("foo is", x.TypeOf("foo")) fmt.Println("0.1234 is", x.TypeOf(0.1234)) }
実行結果
refresh.. in X drop table foo.Target in Base create table foo.Target ---------------------------------------- 10 is Integer foo is String 0.1234 is Unknown
実際の利用例
xormは以下の様な感じでの実際に利用している。
- Dialectがインターフェイス https://gitea.com/xorm/xorm/src/branch/master/dialects/dialect.go#L42-L73
- Baseがデフォルト実装(helperの実装) https://gitea.com/xorm/xorm/src/branch/master/dialects/dialect.go#L76
- posgresが拡張実装 https://gitea.com/xorm/xorm/src/branch/master/dialects/postgres.go#L782