goで想定した実装が1つだけのときに、テストのためだけにインターフェイスを書きたくない
この話に似た話。type aliasを使った型の読み替えで手抜きができる(場合がある)。
リンク先の記事はtype aliasを使って同一パッケージ上にimportしてくる。 そしてテスト中はmockの方にビルドタグで切り替えている。
この記事ではそれとは違った形でtype aliasを使って手抜きをすることを試みてみる。
TL;DR
詳細
例えば以下の様なインターフェイスをイメージしたコードがあるとする。例えばRSSのフィードをパースしてきて記事のタイトルを集めるようなものだとする。Get()
には本来は外部との通信などめんどくさそうな処理が入る。
// News ... type News interface { Get() ([]string, error) }
というインターフェイスになっていると想定しているものの、本当にこのようなインターフェイスで実装していくつもりかどうかは定かではない。あるいはテストのためだけにインターフェイスは切りたくない。
type aliasを使った型の読み替えで手抜きができる(場合がある)
そういうインターフェイスを定義するかどうか迷うようなはじめの段階で、type aliasを使った型の読み替えで手抜きをすることができる。かもしれない。
本来はインターフェイスとして定義するメソッド部分を直接引数として受け取る構造体を作る。これのポインタをtype aliasで名前をつけてあげると他の箇所ではあたかもインターフェイスであるかのように使える。
package news // FuncNews ... type FuncNews struct { Get func() ([]string, error) } // News ... type News *FuncNews // New ... func New() News { return &FuncNews{Get: // ここでは正しい実装を} }
例えば上のコードのNewsは、利用する側からみた場合には、あたかもNewsというインターフェイスがあるかのように振る舞ってくれる。
テストでもそのまま使える
テストを書くためにモックを作ることもインターフェイスを満たしたfakeをもう一度作る必要もない。
package news import ( "fmt" "io" "strings" "testing" ) func Use(title string, news News, w io.Writer) error { fmt.Fprintf(w, "# %s\n", title) fmt.Fprintln(w, "") titles, err := news.Get() if err != nil { return err } for _, title := range titles { fmt.Fprintf(w, "- %s\n", title) } return nil } func TestIt(t *testing.T) { news := &FuncNews{ Get: func() ([]string, error) { return []string{ "foo", "bar", "boo", }, nil }, } var b strings.Builder if err := Use("my news", news, &b); err != nil { t.Fatalf("something wrong: %s", err) } want := `# my news - foo - bar - boo ` got := b.String() if want != got { t.Errorf("want %s, but %s", want, got) } }
最初の実装としては悪くない手抜きなのではないかと思う。
注意点
注意点をあげるとすると、structとそのメソッドで実装するようなふつうの方法で定義した場合は、この実装からみてflyweight patternっぽい形に見える点。つまるところ、この方法では作った値の数だけクロージャを保持することになる。たくさん値を作るような処理ではやらないほうが良いかもしれない。
もちろん、これはインターフェイスではないのでtype assertionを使うことはできない。