Golang 初めての Unit テスト #golang


以前の記事（[Golang と Line Notify を利用して API の証明書期限切れチェック](https://qiita.com/ynozue/items/d00a76e16d5a3d5a784c)）を作りましたが今回は単体テストを追加します。

# 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")
	}
}
```

## テストを実行
```sh
$ 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
![Kobito.uPref4.png](https://qiita-image-store.s3.amazonaws.com/0/203864/d962ea0c-f0a9-2921-89c5-b9f4ffe1fb05.png "Kobito.uPref4.png")

```sh
$ 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
* [ソースコード](https://github.com/ynozue/apichecker/tree/unittest)