$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

golangでAPIなど外部にアクセスするロジックのテストをする

golangで、例えばGithubAPIを叩くような、特定のAPIにアクセスするロジックを書いた時、何も考えずにテストを書くと、テストを実行する際にもそのまま外部のAPIにアクセスしてしまう。この場合、色んなパターンのテストを書きづらい、依存している外部サービスが落ちたらテストも一緒に落ちるなどの問題が起こる。

このような問題から、統合テストではなくユニットテストのときは手元のみで完結して、外部サービスに依存しない状況でテストを書きたくなることがある。そこで今回は外部にアクセスするロジックを、手元で完結させた状態でテストする方法を試したので、その方法について書いてみる。

テストしたいコード

例えば以下のようなコード。Githubhttps://github.com/shibayu36/shibayu36 の最新のリリースタグを取得し、そのリリースタグ名を出力する。これはGithubReleases 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())
}

テストを書くための作戦

上記のような実装をそのままテストしようとすると、

  • テスト中にGithubAPIにアクセスしてしまう
  • Github上に様々なデータを作らないと、色んなパターンのテストが出来ない

という問題がある。

そこで以下のような作戦でテストを書けるようにする。

  • 実装はstructのメソッドとして定義し、APIのベースのURLを切り替え可能にする
  • テスト中はhttp.NewServeMux() + httptestを利用し、テスト用のサーバを立てる

ではこの二点をそれぞれ対応してみる。

実装はstructのメソッドとして定義し、APIのベースのURLを切り替え可能にする

まず先程のコードをテストしやすいように、「GithubAPIにアクセスし最新のリリースタグ名を取得する」という部分をメソッド化する。そのメソッドを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を切り替えることができる
  • githubAPIを叩く時に、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なのでエラー")
	}
}

まとめ

今回はgolangで外部にアクセスするロジックを手元で完結させた状態でテストする方法について書いてみた。

テスト用のダミーサーバを立てるには、http.NewServeMux() + httptestが非常に便利だった。golangの場合、テスト時にgithub.Client.Repositories.GetLatestReleaseを置き換えるみたいなことが出来ないため、実装部分でうまく置き換えられるようにしておくのが少し難しかった。

ちなみにmkrのプラグインインストーラも今回の作戦でテストをしているので参考にどうぞ。

構造が人間の行動を決定する - 「学習する組織」を読んだ

組織関連の興味の一貫として、「学習する組織」を読んだ。

この本は組織の学習能力に影響を与える五つの重要なコンセプトについて教えてくれる本。特に副題となっている、システム思考という考えを重点的に教えてくれる。600ページ近くある本で読み切るのは結構大変な感じではあった。

読んでみたら、まず最初20%の、なぜチームの学習障害は発生するのかを具体的にビールゲームという話題で教えてくれる部分と、システム思考の話は非常に面白かった。この二つは自分に新しい視点を与えてくれた。この部分を読むのはおすすめ。

一方で、それ以降は結構難解で抽象的なので、自分はいまいち分からないなという感想を抱いた。なので最初20%くらいはちゃんと読んでたけど、流し読みという感じにはなった。後半部分は人を選びそうだった。


この本の中で僕が面白いと思ったのは、構造が人間の行動を決定し、同じ構造なら別の人に変えても同じような結果になりやすいという話。確かにある問題を対処しようと細かい対策をしても全く変わらなかったものが、チームのコミュニケーションの流れを少し調整したり、組織構造を調整したりと全体を見て調整をすることで、今まで全く解決しなかった問題が劇的に改善することがあるので、この話は面白いなと思う。

構造が人間の行動を決定するというのがあるので、ある問題が起こった時、その間近だけ見ていては問題が起こる理由が分からないことがある。そのためにシステム思考という考え方で、その問題を根本的に起こしている構造を明らかにすることが大事だと書かれていた。システム思考自体の考え方は説明が難しいので本を読んでもらうと良さそう。

読書メモ

  • 学習する組織とは、目的を達成する能力を効果的に延ばし続ける組織であり、その目的は皆が望む未来の創造 10
  • チームの中核的な学習能力 223
    • 志の育成: 自己マスタリー、共有ビジョン
    • 内生的な会話の展開: メンタルモデル、ダイアログ
    • 複雑製の理解: システム思考
  • 学習する組織にするための五つの要素技術 428
    • システム思考
    • 自己マスタリー
    • メンタルモデル
    • 共有ビジョン
    • チーム学習
  • 七つの学習障害 726
    • 私の仕事は○○だから: 組織内の人たちが自分の職務にだけ焦点を当てていると、すべての職務が相互に作用した時に生み出される結果に責任をほとんどもたない
    • 悪いのはあちら
    • 先制攻撃の幻想
    • 出来事への執着
    • ゆでガエルの寓話
    • 「経験から学ぶ」という妄想: 最善の学習は経験を通じた学習だが、もっとも重要な意思決定がもたらす結果を直接には経験できないことが多い
    • 経営陣の神話
  • ビール・ゲームの教訓 1182
    • 構造が挙動に影響を与える: 人が替わっても、同一の構造の中では定性的に同じような結果を生み出す傾向がある
    • 人間のシステムにおける構造はとらえにくい
    • レバレッジは往々にして新しい考え方によってもたらされる
  • 構造が挙動に影響を与える 1248 ※
    • 重要な問題を理解するためには個人の間違いや不運を超えた見方をしなければならない。個々の行動を形作り、ある種の出来事が起こりやすい状況を作り出している、根底にある構造に目を向けなければならない
  • 大半の人は、自分の仕事をシステムの残りの部分と切り離して、「自分の役をうまくやること」が自分の仕事だと考える。必要なのは、その役がより大きなシステムとどのように相互作用しているかを理解すること 1392
  • 自分が行動することで、「外的要因」ととらえている諸変数に影響を与えていることを忘れてはいけない 1406
  • 学習障害とビール・ゲーム 1448
  • なぜ構造の説明が非常に重要かというと、それをもってしか、挙動パターンそのものを変えられるレベルで、挙動の根底にある原因に対処することができないからだ。構造が挙動を生み出すゆえに、根底にある構造を変えることで異なる挙動パターンを生み出すことが出来る 1494
  • フィードバック・ループ図 1911
  • フィードバック・プロセスには、自己強化型とバランス型の二つの異なった型がある 1982
  • 多くのフィードバック・プロセスには「遅れ」が伴い、徐々に行動の結果をもたらす「影響の流れ」を中断させる 1982
  • システム原型1: 成長の限界 2280
  • システム原型2: 問題のすり替わり 2439
  • ビジョンは共有されるプロセスが重要。組織中のあらゆる人々の個人ビジョンと結びつかないといけない 4505

マネジメントの要素を知る - 「マネジメント入門」を読んだ

マネジメントの技術全体に興味があるので、その要素にはどういうものがあるかを知っておくために「マネジメント入門」を読んだ。

この本は、マネジメントにはどういう話題があり、それぞれにはどのような研究や考えがあるかについて、ざっくり概要を教えてくれる本だった。マネジメントの機能を、計画する、組織する、リーダーシップを発揮する、コントロールするの四つに分類して話を進めている。目次は以下のとおり。

  • イントロダクション: マネジャーとマネジメント・マネジメント環境・マネジメント全般に関わる課題
  • 計画する: 意思決定の基礎・計画策定の基本
  • 組織する: 組織の構造と設計・人材を管理する・変革とイノベーションのマネジメント
  • リーダーシップを発揮する: 個人行動の基礎・グループを理解し、業務チームをマネジメントする・従業員のモチベーションを高める・リーダーシップと信頼・マネジメントコミュニケーションと情報
  • コントロールする: コントロールの基礎・オペレーションマネジメント

かなり網羅的に解説されているのでいろんなことを知ることが出来る一方で、それぞれの話題についてはそこまで深く触れられていない。そのため、興味のある分野は別個で他の本をよむ必要があるだろうと感じた。例えば僕が知っている本で興味がある分野だと

意思決定

組織
組織設計を体系的に学ぶ - 「組織デザイン」を読んだ - $shibayu36->blog;

[asin:4798128449:detail]
[asin:4478004595:detail]

リーダーシップ

モチベーション

あたりか。読んでない本はまた読んでみたい。


ちなみに個人的にはこの本よりも、同著者が書いている「マネジメントとは何か」の方が面白かったかも。
「ベストを尽くせ」だけではベストは生まれない - 「マネジメントとは何か」を読んだ - $shibayu36->blog;
[asin:4797372605:detail]

読書メモ

  • 目次からマネジメントに関わる能力が掴める
    • イントロダクション: マネジャーとマネジメント・マネジメント環境・マネジメント全般に関わる課題
    • 計画する: 意思決定の基礎・計画策定の基本
    • 組織する: 組織の構造と設計・人材を管理する・変革とイノベーションのマネジメント
    • リーダーシップを発揮する: 個人行動の基礎・グループを理解し、業務チームをマネジメントする・従業員のモチベーションを高める・リーダーシップと信頼・マネジメントコミュニケーションと情報
    • コントロールする: コントロールの基礎・オペレーションマネジメント
  • マネジメントとは、人を動かしともに働いて、効率的かつ有効に物事を行う活動プロセス 7
    • 効率性 = 無駄がない
    • 有効性 = 目標の高い達成度
  • マネジメントの機能 9
    • 計画する、組織化する、リーダーシップを発揮する、コントロールする
  • どのマネジャーも、計画、組織化、リーダーシップ、コントロールをしているが、地位ごとに活動に費やす時間は違う 16
  • 従業員エンゲージメントの最大の要素は、マネジャーとの人間関係 23
  • 意思決定のプロセスは、問題の特定 -> 意思決定基準の明確化 -> 基準の優先順位付け -> 代替案の作成 -> 代替案の分析 -> 代替案の選択 -> 代替案の実行 -> 解決策の有効性の評価 94
  • 集団の意思決定は個人の意思決定よりも 112
    • 多くの代替案が生まれる
    • 承認を得やすい
    • 正当性を控除させる
    • ただし、デメリットもある 112
  • たいてい集団は個人よりもより良い判断を下す傾向があるが、スピードや創造性を重視するなら個人の方が効果的 114
    • 集団は最低5人、最高でも15人が望ましい。5~7人が効果的
  • 個人の創造性の発揮には、専門知識、創造的思考力、内発的動機づけが必要 118
  • 戦略的マネジメントのプロセス 148
  • 学習する組織とは、組織構成員が理念を共有しながら、行動と学習を自発的に継続することで、組織全体の能力が高まっていく組織 204
    • 異なる職能・組織レベルでも組織全体で情報を共有し業務で協力することが不可欠
  • 組織行動(OB)は個人行動、集団行動、組織的側面の3分野に注目する 283