ようやくGoを勉強し始めました。実装した関数のテストコードを書きながら、Goのインターフェースの面白さを実感しているところです。
たとえば、下記のWebsiteIsUp関数を実装したとします。引数で渡されたURLにGETでアクセスし、ステータスコードが200ならtrue、それ以外ならfalseを返す関数です。
func WebsiteIsUp(url string) (bool, error) { resp, err := (&http.Client{}).Get(url) if err != nil { return false, err } return (resp.StatusCode == http.StatusOK), nil }
しかし、この関数はテストしづらいものです。テストする立場に立てば、テスト時には *http.Client のGetメソッドをモックしたくなります。モックしないと、テストの成功/失敗がアクセス先のURLに依存してしまうからです。
というわけで、モックを渡せるようにWebsiteIsUp関数に引数を追加しましょう。さて、どんな型の引数が適切でしょうか?
そこで使えるのがインターフェースです。具体的には、*http.Client のGetメソッドと同じシグネチャのGetメソッドを持つインターフェースです。
type HTTPGetter interface { Get(url string) (*http.Response, error) }
このHTTPGetterインターフェースを定義すると、*http.Client もHTTPGetterインターフェースを持つことになります。面白いですね。この仕組みなら、他のパッケージの構造体がいくらでもモックできます。
さて、WebsiteIsUp関数に引数を追加してみましょう。
func WebsiteIsUp(url string, g HTTPGetter) (bool, error) { resp, err := g.Get(url) if err != nil { return false, err } return (resp.StatusCode == http.StatusOK), nil }
これで、必要に応じてモックが渡せます。やった。
type notFoundGetter struct{} func (g *notFoundGetter) Get(url string) (*http.Response, error) { return &http.Response{StatusCode: http.StatusNotFound}, nil } func main() { isUp, err := WebsiteIsUp("http://example.com/", ¬FoundGetter{}) if err != nil { log.Fatal(err) } fmt.Println(isUp) }
ちなみに、モックしない版のコード例全体はこんな感じです。いやあ面白い。
package main import ( "fmt" "log" "net/http" ) type HTTPGetter interface { Get(url string) (*http.Response, error) } func WebsiteIsUp(url string, g HTTPGetter) (bool, error) { resp, err := g.Get(url) if err != nil { return false, err } return (resp.StatusCode == http.StatusOK), nil } func main() { isUp, err := WebsiteIsUp("http://example.com/", &http.Client{}) if err != nil { log.Fatal(err) } fmt.Println(isUp) }