Goでシンプルに単体テストを書く

`Go`の標準ライブラリである`testing`を使ってテストを書くお話です。

いわゆる**単体テスト**と呼ばれるレベルでのテストを書いていきます。

## テストするコード

テストコードを書くお題には次のような関数を使いました。


``` color.go
package main

import (
	"fmt"
)

func showColor(fruit string) (string, error) {
	fruitBasket := map[string]string{
		"banana": "yellow",
		"apple":  "red",
		"melon":  "green",
	}

	if color, ok := fruitBasket[fruit]; ok {
		return color, nil
	} else {
		return color, fmt.Errorf("%s does not exist in Fruit Basket", fruit)
	}
}
```

果物の名前を放り込むと何色かを返してくれる関数ですね。(フルーツバスケット的な)

関数の中で定義されている`fruitBasket`に存在しない果物の名前に対してはエラーを返すようにしています。


## テストを書く

`Go`のお作法では`color.go`のテストコードは`color_test.go`というファイルに記述します。


``` color_test.go
package main

import (
  "testing"
)

func Test_showColor(t *testing.T) {
  tests := []struct {
    name      string
    fruit     string
    wantColor string
    wantErr   bool
  }{
    {
      name:      "正常系1 バナナ",
      fruit:     "banana",
      wantColor: "yellow",
      wantErr:   false,
    },
    {
      name:      "正常系2 りんご",
      fruit:     "apple",
      wantColor: "red",
      wantErr:   false,
    },
    {
      name:      "正常系3 メロン",
      fruit:     "melon",
      wantColor: "green",
      wantErr:   false,
    },
    {
      name:      "異常系 いちご",
      fruit:     "strawberry",
      wantColor: "",
      wantErr:   true,
    },
  }

  for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
      actualColor, err := showColor(tt.fruit)
      if tt.wantErr {
        // tt.wantErr が true つまりちゃんとエラーが発生することを確認するための分岐
        if err == nil {
          // エラーが発生するはずなのに err == nil つまりコードかテストケースにバグがある
          t.Fatalf("%q. wantErr %v, but actual err %v", tt.name, tt.wantErr, err)
        }
      } else if err != nil {
        // 正常終了するはずなのにエラーが発生 同じくコードかテストコードにバグがある
        t.Fatalf("%q. wantErr %v, but actual err occured %+v", tt.name, tt.wantErr, err)
      } else {
        // 関数が正常終了したので返り値が期待通りか検証する
        if tt.wantColor != actualColor {
          t.Fatalf("%q. expected color %v, but actual %v", tt.name, tt.wantColor, actualColor)
        }
      }
    })
  }
}
```

`Go`には`assert`のような関数がないため、`t.Run()`でテストを走らせた後に自分で一つ一つ値を検証していくことになります。


## テストを実行する
下記のコマンドでテストを実行できます。

`FAIL`した場合、テストコードか元のコードに問題があるため、**何故FAILしたのかを把握しやすいようにログメッセージを書いておくのがコツです。**

```
$ go test 

PASS
ok  	github.com/hogehoge/go-testing-sample	0.005s


(以下、FAILした場合の出力)
--- FAIL: Test_showColor (0.00s)
    --- FAIL: Test_showColor/正常系3_メロン (0.00s)
        color_test.go:55: "正常系3 メロン". expected color pink, but actual green
FAIL
exit status 1
FAIL	github.com/hogehoge/go-testing-sample	0.006s
```

上記のコマンドだと`Go`で書かれたテストコードが**すべて実行**されてしまいます。

任意のテストだけ動かしたい場合には下記のように実行します。今回であれば`Test_showColor`のみ実行したいため...

```
$ go test -run Test_showColor

PASS
ok  	github.com/hogehoge/go-testing-sample	0.005s
```

## カバレッジの確認
下記のようにテストを実行すると、実行結果とともにテストコードのカバレッジも出力してくれます。

```
$ go test -cover

PASS
coverage: 100.0% of statements
ok  	github.com/hogehoge/go-testing-sample	0.008s
```

ちなみにコードのどの部分までカバーできているのかを確認するには下記のようにします。

```
$ go test -coverprofile cover.out

PASS
coverage: 100.0% of statements
ok  	github.com/hogehoge/go-testing-sample	0.005s
```

テストを実行したディレクトリ上に`cover.out`というファイルが作成されたかと思います。

こいつを下記コマンドで開きます。

```
$ go tool cover -html=cover.out
```

ブラウザが立ち上がり、下記のような画面が表示されたかと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200260/e2ae5619-39d9-c542-2cf2-00d7b4c4982f.png)


緑の部分が`covered`、つまりテストコードで検証されている部分です。`not covered`は赤色で表示されます。
(今回はカバレッジ100％になりましたが、実際の開発の現場ではカバレッジ100％を目指すことは非常に難しいですし、ナンセンスです)



## 番外編:一連のテスト実行をMakefileにまとめる
`Makefile`で下記のように記述すれば、テストの実行、カバレッジの表示、ブラウザ表示までを一気に実行可能です。

``` 
test:
  go test -coverprofile cover.out &&\
  go tool cover -html=cover.out
```

前述のコマンドを順番に実行するように記述しただけですが、**いちいちプロンプトに入力する手間が省ける**のでご参考までに。

ちなみに`&&`は直前のコマンドが`status 0`で終了した場合に後続のコマンドを実行してくれるものです。

つまり、`go test ~~`がエラーなく完了できた場合のみ、後続の`go too: ~~`を実行してくれます。これにより、`Makefile`では無限にコマンドをつなげる事が可能になります(あんまりやりませんが...)

なお、`Makefile`内でまとめて実行したいコマンド群を改行する際には`\(バックスラッシュ)`を使います。

(おしまい)