地方エンジニアの学習日記

興味ある技術の雑なメモだったりを書いてくブログ。たまに日記とガジェット紹介。

【Go】Interfaceを使ったテストに入門する

チームの共用語(?)がGoになったというのもあってGoのテストについて入門し始めた。テストコード自体はlinux-test-project/ltpを独自でforkしたものをメンテナンスしていたりとCではあるが基礎ぐらいは取得してるかなという感じでした。入門していく中でMockについて学ぶ機会があってこれまであまり触れる機会がないものだったので理解に苦しんだが少しわかったので書いてみる。

そもそもMockに慣れてないのはなぜだろうと考えたら

割とこの記事にあるような環境にいたからなのかなと思ったりした。Mock使うぐらいなら関数に切り出してテストしやすくしたり単体テストで通らないパスはintegrationとかに自動テストを実装しておいてMock不要にしてテストしたりそんな感じでことなきを得てきた(?)。あとはMockを使ってテストではエビデンスとしては足りなくて次工程に回せないなどの特殊な理由もあって個人の開発のしやすさ的に使ってる人はいたかもだけどあまり積極的に書いても出荷判定という壁を越える材料にはならないので使われないみたいなのもあったのかなとか思っている。(これは特殊すぎる例かもしれない...)

github.com

外部APIっぽいものを書く

テストしにくければなんでもよくて例えばhttpアクセスがきたら100までの数字のうちランダムで返すものを用意しておく

package main

import (
    "fmt"
    "math/rand"
    "net/http"
    "strconv"
    "time"
)

func handler(w http.ResponseWriter, r *http.Request) {
    rand.Seed(time.Now().UnixNano())
    fmt.Fprintf(w, strconv.Itoa(rand.Intn(100)))
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

テスト対象のコードを書く

上記のAPIを使ったコードを書くとこんな感じ。テスト対象はRunになるがRunの戻りの文字列はAPIの結果次第で変わってしまうという実装になっている。これを単体テストするとなると取りうる数値を許容するテストを書くかDoHttpRequestで固定値を返すようなオプションを実装するかになってしまう(これまでなら多分そうしてた)。こういう時に使えるのがモックを使ったテストになる。

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
)

func DoHttpRequest(url string) string {
    resp, _ := http.Get(url)
    defer resp.Body.Close()

    byteArray, _ := ioutil.ReadAll(resp.Body)
    return string(byteArray)
}

func Run(s string) string {
    return s + DoHttpRequest("http://localhost:8080") // 引数の文字列と乱数を連結して返す
}

func main() {
    fmt.Println(Run("test: "))
}

mockを使うには

動作を分けたい対象をinterfaceを使って抽象化してテストコードでは対象のinterfaceにテスト用の実装(Mock)をDIするという感じになる。今回分けたい対象はDoHttpRequestなのでこれを使っていくとこんな感じ。interfaceを定義する。

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
)

type APIer interface {
    DoHttpRequest(url string) string
}

type API struct {}

func (a API) DoHttpRequest(url string) string {
    resp, _ := http.Get(url)
    defer resp.Body.Close()

    byteArray, _ := ioutil.ReadAll(resp.Body)
    return string(byteArray)
}

func Run(s string, a APIer) string {
    return s + a.DoHttpRequest("http://localhost:8080")
}

func main() {
    a := &API{}
    fmt.Println(Run("test: ", a))
}

テストコードはこんな感じ。APIMockを定義してinterfaceを実装していくことでmockを使ってRunをテストすることができるようになる。

package main

import (
    "testing"
)

type APIMock struct {}

func (a APIMock) DoHttpRequest(url string) string {
    return ""
}

func TestRand(t *testing.T) {
    ai := &APIMock{}
    tests := []struct {
        a string
        b string
    }{
        {
            a: "test: 1",
            b: "test: 1",
        },
    }

    for _, tt := range tests {
        res := Run(tt.a, ai)
        if tt.b != res {
            t.Errorf("got: %s, want: %s", res, tt.b)
        }
    }

}

interfaceの埋め込み

以下のようにinterfaceが定義されているとする。これをテストしたいのはDoHttpRequestだけの場合にテストコードに全て実装していくのは大変になってしまう。そういう時に使えるのがInterfaceの埋め込み。(これはTDDとかで使えたりするのだろうか)。埋め込み自体はeffective_goとかにも載っていた。

type APIer interface {
    DoHttpRequest(url string) string
    DoHttpRequest2(url string) string
    DoHttpRequest3(url string) string
    DoHttpRequest4(url string) string
    DoHttpRequest5(url string) string
}

qiita.com

type APIMock struct {
    APIer
}

gomock

他言語にもあるようなツールなのであるだろうなと思ってみてたらあった。毎回作るのも面倒臭いしで便利そう。

pkg.go.dev