2020年04月

テストコード追加でOSSに貢献、いい

Echo(GoのWebアプリケーションフレームワーク)に送ったテストコード追加のプルリクエストが、送ってから2時間半後にマージされた。貢献といえば大げさかもしれないが、もし自分がメンテナだったら、テストコード追加のプルリクストがいちばんうれしいはずだ。レビューに時間がかからないところが良い。今後も折を見てトライしたい。

ぼくがたまにOSSに貢献したくなるのは、あけすけにいえば、自分の承認欲求を満たしたいからだ。それが人として善なのかどうかはわからない。ただ、誰かに大きな迷惑をかけるわけではないし、メンテナはうれしいだろうし、ぼくも(一時的には)満足できるし、まったくの悪ではないと思いたい。

現在、Echoのカバレッジは85%だ。まずまずの網羅率とはいえ、Codecovで詳細を見ればわかるとおり、テストしやすそうな部分がけっこう残っている。Echoのメンテナはテストコードを書くのが(ぼくと同じく)おっくうなタイプなのかもしれない。

今さらながらGoのインターフェースが面白かった

ようやく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)
}

CircleCIでcheckoutするならGit入りイメージを使おう

CircleCIのジョブが下記のエラーで失敗していた。

Either git or ssh (required by git to clone through SSH) is not installed in the image. Falling back to CircleCI's native git client but the behavior may be different from official git. If this is an issue, please use an image that has official git and ssh installed.

Enumerating objects: 208, done.
Counting objects: 100% (208/208), done.
Compressing objects: 100% (135/135), done.
Total 1015 (delta 130), reused 134 (delta 61), pack-reused 807

reference not found

git cloneで「reference not found」になっている。イメージにGitクライアントがインストールされておらず、CircleCIネイティブのGitクライアントにフォールバックしたものの、おそらくその実装に問題があるためエラーになったのだろう。

エラーメッセージにあるとおり、公式のGitが入ったイメージを使えば解決しそうだ。

これまでは下記のように「docker」イメージを指定していた。

    docker:
      - image: docker

これを、Git入りのイメージに変えたところ、ジョブが成功した。

    docker:
      - image: docker:git

今まで失敗しなかったのは、たまたま運がよかったのだろう。

プロフィール

ENECHANGE株式会社VPoT兼CTO室マネージャー。AWS Community Builder (Cloud Operations)。前職はAWS Japan技術サポート。社内外を問わず開発者体験の向上に取り組んでいます

カテゴリ別アーカイブ
月別アーカイブ
ブログ内検索