TAKEO

TAKEO

Go インターフェース 単体テスト編

インターフェースの使いどころ

インターフェースを使う理由はいくつかあると思いますが、今回は依存性の注入 (Dependency Injection)に焦点をあてて実装例を紹介したいと思います。

インターフェースを使用して、依存関係を抽象化できます。これにより、テストしやすく、モジュール間の結合度を低くすることができます。

今回対象とするもの

Official Joke APIというジョークをjson形式で返してくれるAPIがあります。このAPIからジョークを取得するCLIを実装例として紹介します。CLIの挙動は次のようにします。

  1. CLIを実行する
  2. https://official-joke-api.appspot.com/jokes/randomにGETリクエストを送る
  3. 取得してきた結果をJSON形式で標準出力する

また、Goのプロジェクトのパッケージ設計は次のようにします。

  • clientパッケージ
    • 純粋なHTTPクライアントであり、外部から提供されたURLに対してHTTPリクエストを実行する部品
  • runnerパッケージ

実装の全てはtake-o20/go-cobra-cli-exampleに置いています。

実装のポイント紹介

take-o20/go-cobra-cli-example/pkgにおける実装のポイントを紹介します。

  • clientパッケージ

    • HttpClientはインターフェースで実装されています

      Copied!!
      package client
      
      type HttpClient interface {
        Get(string) (string, error)
      }
      
  • runnerパッケージ

    • 構造体RandomJokeRunnerはフィールドにHttpClientのインターフェースをもちます

      Copied!!
      package runner
      
      type RandomJokeRunner struct {
        client client.HttpClient
      }
      
    • Runメソッドでclientを使ってリクエストします

      pkg/runner/random_joke_runner.go
      Copied!!
      func (runner *RandomJokeRunner) Run() error {
        url := "https://official-joke-api.appspot.com/jokes/random"
        res, err := runner.client.Get(url)
        if err != nil {
          return err
        }
        ...
        return nil
      }
      

単体テストとインターフェース

runnerパッケージのRunメソッドの単体テストをどのように行うか考えてみましょう。

pkg/runner/random_joke_runner.go
Copied!!
func (runner *RandomJokeRunner) Run() error {
	url := "https://official-joke-api.appspot.com/jokes/random"
	res, err := runner.client.Get(url)
	if err != nil {
		return err
	}
	dst := &bytes.Buffer{}
	if err := json.Indent(dst, []byte(res), "", "  "); err != nil {
		panic(err)
	}
	fmt.Println(dst.String())

	return nil
}

テストケースとしてまず次の2つが考えられるでしょう。

単体テストレベルでは、実際にhttps://official-joke-api.appspot.com/jokes/randomにリクエストを投げるようなことはしません。

それではどのようにそれらのテストを作成するのか?

そうです、ここでインターフェースの効力が発揮されるのです。

Copied!!
package runner

type RandomJokeRunner struct {
  client client.HttpClient
}

RandomJokeRunnerのclientフィールドはインターフェースで定義されています。このclientを単体テストの時だけ、全く別物に置き換えることによって、前述したテストケースを実装することができます。

例えば次のような構造体を自前で定義してあげます。

Copied!!
type mockHttpClient struct {
	mockResponse string
	mockError    error
}

func (m *mockHttpClient) Get(string) (string, error) {
	return m.mockResponse, m.mockError
}

RandomJokeRunnerのクライアンとして持たせてあげることができます。

Copied!!
runner := RandomJokeRunner{
  client: &mockHttpClient{}
}

このような方法で作成したテストは下記に記載しています。参考にしてください。

まとめ・所感

今回はインターフェースの使いどころを単体テストに焦点をあてて紹介しました。

今回紹介したCLIの実装はtake-o20/go-cobra-cli-exampleにあります。

個人的にインターフェースを使う意義を最も理解できたのは単体テストを実装するようになってからでした。