読者です 読者をやめる 読者になる 読者になる

成らぬは人の為さぬなりけり

エンジニアライフをエンジョイする為のブログ

Goでテストを書く

久しぶりにちゃんとgolangを勉強していこうという事で、ログを残します。

今日のテーマ

  • testingパッケージを使ってテストを書いてみる
  • gomgospelをインストール
  • gospelでテスト書いてみる
  • 独自matcherを書いてみる

環境

構成

$ tree .
.
├── Gomfile
├── src
│   └── model
│       ├── user.go
│       ├── user_gospel_test.go
│       └── user_test.go
└── vendor

testingパッケージを使ってテストを書いてみる

まずは、標準のお作法にしたがってテストを書いてみます。

テスト対象はこんな感じです。

user.go

package model

import (
    "strings"
)

type User struct {
    FirstName string
    LastName string
    Divisions []string
}

func NewUser(name string) *User {
    names := strings.Split(name, " ")
    if len(names) >= 2 {
        return &User{FirstName: names[0], LastName: names[1]}
    } else {
        return &User{FirstName: names[0]}
    }
}

func (user *User) FullName() string{
    return user.FirstName + " " + user.LastName
}

func (user *User) AddDivision(division string) *User{
    user.Divisions = append(user.Divisions, division)
    return user
}

さて、テストを書いてみます。

user_test.go

package model

import (
    "testing"
)

func TestConstructorWithFullname(t *testing.T) {

    user := NewUser("hoge aaa")

    if user.FirstName != "hoge" {
        t.Error("user's first name should be hoge")
    }

    if user.LastName != "aaa" {
        t.Error("user's last name should be aaa")
    }

}

func TestConstructorWithFirstName(t *testing.T) {

    user := NewUser("hoge")

    if user.FirstName != "hoge" {
        t.Error("user's first name should be hoge")
    }

    if user.LastName != "" {
        t.Error("user's last name should be empty")
    }

}

func TestConstructorWithEmptyString(t *testing.T) {

    user := NewUser("")

    if user.FirstName != "" {
        t.Error("user's first name should be empty")
    }
    if user.LastName != "" {
        t.Error("user's last name should be empty")
    }

}

func TestDevision(t *testing.T) {
 
    user := NewUser("hoge aaa")

    if len(user.Divisions) != 0 {
        t.Error("default divisions is empty slice")
    }

}

func TestFullName(t *testing.T) {

    fullname := "hoge aaa"
    user := NewUser(fullname)

    if user.FullName() != fullname {
        t.Errorf("fullname should be %s, but %s", fullname, user.FullName())
    }
}

func TestAddDevision(t *testing.T) {
 
    user := NewUser("hoge aaa")
    division := "test"

    user.AddDivision(division)
    if user.Divisions[0] != division {
        t.Log(user.Divisions)
        t.Errorf("%s division was not added", division)
    }

}
  • xxx_test.goというファイル名にする
  • テスト関数はTestというprefixをつける
  • アサーションは自分でifなどでチェックしてt.Errorでfailさせる

testingにはアサートしてくれる関数がありません。 その辺の考え方的には

Go の Test に対する考え方 - Qiita

あたりを見るとよく分かります。 ふむ。

では、テスト実行してみます。

まずは、GOPATHをカレントディレクトリにしておきます。

$ export GOPATH=`pwd`

したら、実行します。

$ go test -v ./src/...
=== RUN TestConstructorWithFullname
--- PASS: TestConstructorWithFullname (0.00 seconds)
=== RUN TestConstructorWithFirstName
--- PASS: TestConstructorWithFirstName (0.00 seconds)
=== RUN TestConstructorWithEmptyString
--- PASS: TestConstructorWithEmptyString (0.00 seconds)
=== RUN TestDevision
--- PASS: TestDevision (0.00 seconds)
=== RUN TestFullName
--- PASS: TestFullName (0.00 seconds)
=== RUN TestAddDevision
--- PASS: TestAddDevision (0.00 seconds)
PASS
ok      model   0.010s

全部通りました。

gomgospelをインストール

依存パッケージはやっぱり設定ファイルとかで管理したいので、今回はgomを使ってみます。 (gondlerの方がスタンダードなのかな?依存パッケージのバージョンとか、あまり管理しないのかな?)

まずはgomをインストール

ここではgo getつかいます。

$ go get github.com/mattn/gom

Gomfileを書く

gom 'github.com/r7kamura/gospel', :commit => 'd575dd12c2eb84612ae5c84fab56ccb4ce156a1e'

Rubyっぽい... mattn/gom · GitHub とか見てもほぼGemfileですね。 tagも使えるし、groupも使える。

じゃあ、こんなのもいけるのか??

gom 'github.com/r7kamura/gospel', commit: 'd575dd12c2eb84612ae5c84fab56ccb4ce156a1e'

$ gom install
gom:  Syntax Error at line 1

怒られました... orz

気を取り直して、Gomfileを元に戻してインストールしなおします。

$ gom install
downloading github.com/r7kamura/gospel

無事完了。

gospelでテスト書いてみる

では、使い方を参考に書いてみます。

user_gospel_test.go

package model

import (
    . "github.com/r7kamura/gospel"
    "testing"
)

func TestUser(t *testing.T) {

    Describe(t, "NewUser", func() {

        Context("フルネーム(hoge aaa)を指定した場合", func() {
            user := NewUser("hoge aaa")

            It("FirstName should be hoge", func() {
                Expect(user.FirstName).To(Equal, "hoge")
            })

            It("LastName should be aaa", func() {
                Expect(user.LastName).To(Equal, "aaa")
            })

        })

        Context("空文字を渡した場合", func() {
            user := NewUser("")

            It("FirstName should be empty string", func() {
                Expect(user.FirstName).To(Equal, "")
            })

            It("LastName should be empty string", func() {
                Expect(user.LastName).To(Equal, "")
            })

        })

    })
    Describe(t, "Divisions", func() {
        user := NewUser("")

        It("default divisions is empty slice", func() {
            Expect(len(user.Divisions)).To(Equal, 0)
        })

    })

}

Rspec風に書けてBDDな感じで書きやすいですね。

実行する時はgom exec使います。 bundle execみたいな。

$ gom exec go test -v ./src/...
=== RUN TestUser
NewUser
  フルネーム(hoge aaa)を指定した場合
    FirstName should be hoge
    LastName should be aaa
  空文字を渡した場合
    FirstName should be empty string
    LastName should be empty string
Divisions
  default divisions is empty slice
PASS
ok      model   0.011s

※ 急に日本語で書いたのは気にしない ※ だいぶテストケースが減ってるのも気にしない

結果も見やすくて良い感じです。 (ちなみに、Describeをネストするとエラーになりました)

独自matcherを書いてみる

スライスとか配列のlengthをチェックするmatcherとかあっても良いかなぁという気がしていて、 この辺とか見ていたら、 自分で書けそうなので、書いてみました。

package model

import (
    . "github.com/r7kamura/gospel"
    "testing"
    "fmt"
    "reflect"
)

func EqualLength(values ...interface{}) (failureMessage string) {
    actualValue := reflect.ValueOf(values[0])

    if actualValue.Kind() != reflect.Slice && actualValue.Kind() != reflect.Array {
        failureMessage = fmt.Sprintf("`%v` is not slice or array", values[0])
        return
    }

    if actualValue.Len() != values[1] {
        failureMessage = fmt.Sprintf("Expected `%v` length `%v` to equal `%v`", values[0], actualValue.Len(), values[1])
    }
    return
}

func TestUser(t *testing.T) {

    Describe(t, "Divisions", func() {
        user := NewUser("")

        It("default divisions is empty slice", func() {
            Expect(user.Divisions).To(EqualLength, 0)
        })

    })

}

reflectパッケージ使って、TypeがSliceかArrayならlengthチェックするみたいな事をしています。

ためしに、

It("length", func() {
    Expect([]string{"hoge"}).To(EqualLength, 0)
})
It("length", func() {
    Expect([1]string{"hoge"}).To(EqualLength, 0)
})
It("length", func() {
    Expect(100).To(EqualLength, 0)
})

こんなんでfailさせてみます。

=== RUN TestUser
Divisions
  length
  Expected `[hoge]` length `1` to equal `0`
  /Users/natsuki/workspace/go-samples/test/unit-test/src/model/user_gospel_test.go:63
    62.    It("length", func() {
    63.      Expect([]string{"hoge"}).To(EqualLength, 0)
    64.    })
  length
  Expected `[hoge]` length `1` to equal `0`
  /Users/natsuki/workspace/go-samples/test/unit-test/src/model/user_gospel_test.go:66
    65.    It("length", func() {
    66.      Expect([1]string{"hoge"}).To(EqualLength, 0)
    67.    })
  length
  `100` is not slice or array
  /Users/natsuki/workspace/go-samples/test/unit-test/src/model/user_gospel_test.go:69
    68.    It("length", func() {
    69.      Expect(100).To(EqualLength, 0)
    70.    })
--- FAIL: TestUser (0.00 seconds)
FAIL
exit status 1
FAIL    model   0.009s
gom:  exit status 1

おぉ、悪くないかも。

まとめ

  • golangはテストを大事にしている
  • テストコードとディレクトリを分けない文化はめずらしい気がした
    • これもテスト重要っていう文化故になのか
  • GomfileはRubyっぽいだけでRubyではない(?)
  • go testベンチマークも取れるし、カバレッジも取れるっぽい
    • go test -hでいろいろ出てくる
    • -coverオプション付けるとなんか色々足りないって('go get`しろって)怒られる
  • gospelのMatcherは結構簡単に書ける