WHITEPLUS TechBlog

GoLandで書く効率的なGoのユニットテスト(Table Driven Test)

この記事はWHITEPLUS Advent Calendar 2018 - Qiita 5日目になります。

こんにちは、WHITEPLUSのサーバーサイドエンジニア八巻です。 マネージャーをしながら、リネットの設計リードをしたりしています。


今日は設計の検証に欠かせないユニットテストについて書きます。

ユニットテストのパターンの一つとしてData Driven Testというものがあります。Goの場合はTable Driven Testとして知られていて基本的には同じものです。

package stringutil

import "testing"

func TestReverse(t *testing.T) {
    cases := []struct {
        in, want string
    }{
        {"Hello, world", "dlrow ,olleH"},
        {"Hello, 世界", "界世 ,olleH"},
        {"", ""},
    }
    for _, c := range cases {
        got := Reverse(c.in)
        if got != c.want {
            t.Errorf("Reverse(%q) == %q, want %q", c.in, got, c.want)
        }
    }
}

How to Write Go Code - The Go Programming Language

TableDrivenTests · golang/go Wiki · GitHub

これらの公式ドキュメントにかかれている通りデータのパターンを変えながら同じテストを回すことで、テストの可読性が増します。

GoLandでは自作の関数やメソッドに対して簡単にTable Driven Testを書く機能がついています。

例として期間を扱うTimeSpan型を考えてみます。

type TimeSpan struct {
    start time.Time
    end   time.Time
}

func New(start, end time.Time) (TimeSpan, error) {
    if end.Before(start) {
        return TimeSpan{}, errors.New("endはstartよりも前ではいけません")
    }
    return TimeSpan{start: start, end: end}, nil
}

この状態で関数Newに対して「Go To」から「Test」を選択します。 f:id:yamakii:20181204001502p:plain

f:id:yamakii:20181204001556p:plain

そうするとTable Driven Testの雛形が作成されます。 f:id:yamakii:20181204001738p:plain

中身を見ていきます。

argsは関数に渡す引数を表す型です。

そして各テストケースはstructになっています。

  • name: テストの名前
  • args: 関数に渡す引数
  • want: 予想結果
  • wantErr: エラー発生の有無

関数の検証は自動で用意されています。

Todoで書かれている部分を実際に埋めてみます。

func TestNew(t *testing.T) {
    type args struct {
        start time.Time
        end   time.Time
    }
    tests := []struct {
        name    string
        args    args
        want    TimeSpan
        wantErr bool
    }{
        {
            name: "start = end",
            args: args{
                start: time.Date(2018, time.December, 5, 0, 0, 0, 0, time.Local),
                end:   time.Date(2018, time.December, 5, 0, 0, 0, 0, time.Local),
            },
            want: TimeSpan{
                start: time.Date(2018, time.December, 5, 0, 0, 0, 0, time.Local),
                end:   time.Date(2018, time.December, 5, 0, 0, 0, 0, time.Local),
            },
        },
        {
            name: "start < end",
            args: args{
                start: time.Date(2018, time.December, 5, 0, 0, 0, 0, time.Local),
                end:   time.Date(2018, time.December, 5, 0, 0, 0, 1, time.Local),
            },
            want: TimeSpan{
                start: time.Date(2018, time.December, 5, 0, 0, 0, 0, time.Local),
                end:   time.Date(2018, time.December, 5, 0, 0, 0, 1, time.Local),
            },
        },
        {
            name: "start > end",
            args: args{
                start: time.Date(2018, time.December, 5, 0, 0, 0, 0, time.Local),
                end:   time.Date(2018, time.December, 4, 0, 0, 0, 0, time.Local),
            },
            want:    TimeSpan{},
            wantErr: true,
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := New(tt.args.start, tt.args.end)
            if (err != nil) != tt.wantErr {
                t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if !reflect.DeepEqual(got, tt.want) {
                t.Errorf("New() = %v, want %v", got, tt.want)
            }
        })
    }
}

f:id:yamakii:20181204003024p:plain
実行結果

自分で書くとなると複雑になりがちなTable Driven Testですが自動生成を利用すると自分で書く部分はテストケースだけになります。

これによって、よりテストケースの洗い出しに注意を向けることができるので、テストの質が上がります。

また、各テストケースに名称がつくので、どのパターンがエラーになったかひと目で分かります。

とても便利な機能なのでGoLandでテストを書く際にはぜひ利用してみてください。

今回使ったコードはこちらです。 https://github.com/yamakii/golang-sample/tree/master/timespan


明日は来年(2019年)の4月に新卒入社予定の飯塚くんの記事です。

ホワイトプラスではGoやPHPでコードがサービスの資産となるようにメンテナンス性や設計に気を配りながら働く仲間を募集してます。

www.wantedly.com