golangで、例えばGithubのAPIを叩くような、特定のAPIにアクセスするロジックを書いた時、何も考えずにテストを書くと、テストを実行する際にもそのまま外部のAPIにアクセスしてしまう。この場合、色んなパターンのテストを書きづらい、依存している外部サービスが落ちたらテストも一緒に落ちるなどの問題が起こる。
このような問題から、統合テストではなくユニットテストのときは手元のみで完結して、外部サービスに依存しない状況でテストを書きたくなることがある。そこで今回は外部にアクセスするロジックを、手元で完結させた状態でテストする方法を試したので、その方法について書いてみる。
テストしたいコード
例えば以下のようなコード。Githubの https://github.com/shibayu36/shibayu36 の最新のリリースタグを取得し、そのリリースタグ名を出力する。これはGithubのReleases APIにアクセスしている。
package main import ( package main import ( "context" "fmt" "github.com/google/go-github/github" ) func main() { ctx := context.Background() client := github.NewClient(nil) release, _, _ := client.Repositories.GetLatestRelease(ctx, "shibayu36", "shibayu36") fmt.Println(release.GetTagName()) }
テストを書くための作戦
上記のような実装をそのままテストしようとすると、
という問題がある。
そこで以下のような作戦でテストを書けるようにする。
- 実装はstructのメソッドとして定義し、APIのベースのURLを切り替え可能にする
- テスト中はhttp.NewServeMux() + httptestを利用し、テスト用のサーバを立てる
ではこの二点をそれぞれ対応してみる。
実装はstructのメソッドとして定義し、APIのベースのURLを切り替え可能にする
まず先程のコードをテストしやすいように、「GithubのAPIにアクセスし最新のリリースタグ名を取得する」という部分をメソッド化する。そのメソッドをmain関数で呼んで、リリースタグ名を出力する。
package main import ( "context" "fmt" "net/url" "github.com/google/go-github/github" ) type myClient struct { apiGithubURL string } // ownerとrepoから最新のリリースタグ名を取得するメソッド func (c *myClient) GetLatestReleaseTagName(owner, repo string) (string, error) { ctx := context.Background() client := github.NewClient(nil) // Github APIのベースURLを切り替えられるようにしておく client.BaseURL = c.getAPIGithubURL() release, _, err := client.Repositories.GetLatestRelease(ctx, owner, repo) if err != nil { return "", err } return release.GetTagName(), nil } // go-githubのクライアントのClient.BaseURLに渡せるURLオブジェクト作成 // myClientインスタンス作成時にapiGithubURLを明示的に指定したら、 // それを利用するように。そうでなければデフォルトのURLを使う。 func (c *myClient) getAPIGithubURL() *url.URL { u := "https://api.github.com" if c.apiGithubURL != "" { u = c.apiGithubURL } apiURL, _ := url.Parse(u + "/") return apiURL } func main() { tagName, err := (&myClient{}).GetLatestReleaseTagName("shibayu36", "shibayu36") fmt.Println(tagName, err) }
この部分でのポイントは
- github.com/google/go-github/github で作ったclientはclient.BaseURLにURLオブジェクトを与えることでAPIのベースURLを切り替えることができる
- githubのAPIを叩く時に、myClientのapiGithubURLフィールドが指定されていたらそちらに、そうでなければ普通にapi.github.comを利用するようにclient.BaseURLを指定する
- client.BaseURLにそのまま渡せるオブジェクトを作るgetAPIGithubURLメソッドを作っておくと便利
これで、&myClient{}
でオブジェクトを作ったら普通にGithubにアクセスし、&myClient{apiGithubURL: "http://api.example.com"}
でオブジェクトを作っておくと、http://api.example.com にアクセスするような実装になった。
http.NewServeMux() + httptestを利用し、テスト用のサーバを立てる
実装でアクセス先を切り替えられるようになった。あとはテスト中にダミーのサーバを立てて、アクセス先をそちらに切り替えてテストすれば良い。以下のとおり。これで外部にアクセスせずに、APIなどを使うテストをかくことが出来た。
package main import ( "fmt" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" ) func TestGetLatestReleaseTagName(t *testing.T) { // NewServeMuxを使えば、特定のPATHでレスポンスを返すような // アプリケーションを簡単に書けるので、 // shibayu36/sample-repoの最新リリースAPIを模倣する。 mux := http.NewServeMux() mux.HandleFunc("/repos/shibayu36/sample-repo/releases/latest", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, `{"tag_name": "0.0.1"}`) }) // テスト用に作ったアプリケーションでテストサーバを立てる apiGithubServer := httptest.NewServer(mux) defer apiGithubServer.Close() // APIアクセスを立てたテストサーバに向けてmyClientオブジェクトを作成 c := &myClient{ apiGithubURL: apiGithubServer.URL, } { // 成功パターンのテスト tagName, err := c.GetLatestReleaseTagName("shibayu36", "sample-repo") assert.NoError(t, err) assert.Equal(t, "0.0.1", tagName, "shibayu36/sample-repoのタグが取れている") } { // 失敗パターン(レポジトリがない)のテスト _, err := c.GetLatestReleaseTagName("shibayu36", "wrong-repo") assert.Error(t, err, "存在しないrepoなのでエラー") } }