ようやく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/", &notFoundGetter{})
    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)
}