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

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

緑の部分が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内でまとめて実行したいコマンド群を改行する際には\(バックスラッシュ)を使います。

(おしまい)