Golang 初めての Unit テスト #golang


以前の記事(Golang と Line Notify を利用して API の証明書期限切れチェック)を作りましたが今回は単体テストを追加します。

Golang の単体テストとは

ざっくり調べた感じだと、以下の感じで作ってみたら良さそう

  • テスト対象のコードと同じ階層に置く(標準パッケージが同じ構造になっていた)
  • testing パッケージを利用する
  • テスト用の関数のプレフィックスに「Test」をつける
  • Assert が用意されていないため、自分で条件を記載する
    (テストケースを失敗させたいときは「testing.T.Error」や「testing.T.Fatal」を利用する)
    • testing.T.Error:以降の処理を継続して実行される
    • testing.T.Fatal:以降の処理が実行されない

テストコードを記載

テスト対象のコード

apichecker.go
package main

import (
    "flag"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "net/url"
    "strings"
    "time"
)

func main() {
    var endpoint = flag.String("endpoint", "", "check target Endpoint URL")
    var lineToken = flag.String("token", "", "LINE notify token")
    flag.Parse()

    var apiResult = getAPI(*endpoint)
    var result = postLINE(*lineToken, apiResult)

    fmt.Printf("LINE Post result [%t]\n", result)
}

func getAPI(endpoint string) string {
    if endpoint == "" {
        log.Println("not endpoint")
        return "not endpoint"
    }

    var result = ""
    resp, err := http.Get(endpoint)
    if err != nil {
        result = fmt.Sprintf("NG\n%s", err)
    } else {
        defer resp.Body.Close()
        expire := "-"
        if len(resp.TLS.PeerCertificates) > 0 {
            expireUTCTime := resp.TLS.PeerCertificates[0].NotAfter
            expireJSTTime := expireUTCTime.In(time.FixedZone("Asia/Tokyo", 9*60*60))
            expire = expireJSTTime.Format("06/01/02 15:04")
        }
        result = fmt.Sprintf("OK (expire=%s)\n%s", expire, endpoint)
    }

    return result
}

func postLINE(token string, message string) bool {
    if token == "" {
        log.Println("not token")
        return false
    } else if message == "" {
        log.Println("not text")
        return false
    }

    data := url.Values{"message": {message}}
    r, _ := http.NewRequest("POST", "https://notify-api.line.me/api/notify", strings.NewReader(data.Encode()))
    r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
    r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
    resp, err := http.DefaultClient.Do(r)
    if err != nil {
        log.Println(err)
        return false
    }
    defer resp.Body.Close()
    _, err = ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Println(err)
        return false
    }

    return true
}

getAPI のテストコードを書いてみる

同一パッケージなので、private な関数もテストできて便利
以下のパターンのテストケースを用意

  • パラメータエラー
  • 正常に実行完了
  • 証明書エラー
apichecker_test.go
package main

import (
    "regexp"
    "testing"
)

func TestGetAPI_パラメーターエラー(t *testing.T) {
    if getAPI("") != "not endpoint" {
        t.Error("failed validation check")
    }
}

func TestGetAPI_正常(t *testing.T) {
    r := regexp.MustCompile("OK \\(expire=[0-9]{2}/[0-9]{2}/[0-9]{2} [0-9]{2}:[0-9]{2}\\)\nhttps://www.yahoo.co.jp")
    if !r.MatchString(getAPI("https://www.yahoo.co.jp")) {
        t.Error("function format error")
    }
}

func TestGetAPI_証明書エラー(t *testing.T) {
    r := regexp.MustCompile("NG\n.+")
    if !r.MatchString(getAPI("https://www.yahoo.jp")) {
        t.Error("function format error")
    }
}

テストを実行

$ go test .
ok      github.com/ynozue/apichecker    0.684s

CI にも適用する

設定ファイルにテストの実行を追加

travis.yaml
language: go

install:
 - go get -u golang.org/x/tools/cmd/goimports
 - go get -u github.com/golang/lint/golint

script:
 - go vet ./...
 - diff <(goimports -d .) <(printf "")
 - diff <(golint ./...) <(printf "")
 - go test .

修正した内容を Git へ Push

$ go vet ./...


The command "go vet ./..." exited with 0.
$ diff <(goimports -d .) <(printf "")


The command "diff <(goimports -d .) <(printf "")" exited with 0.
$ diff <(golint ./...) <(printf "")


The command "diff <(golint ./...) <(printf "")" exited with 0.
$ go test .
ok      github.com/ynozue/apichecker    2.600s


The command "go test ." exited with 0.

Appendix