Golangでユニットテスト書くテクニック

Goは他のフレームワークにあるような大きなアサーションツールを持っていません。Goでは testing.T オブジェクトのメソッドがテストに使われます。

  • T.Error(args ...interface{}) または T.Error(msg string, args interface{}) はメッセージを受け取ってテストを失敗させるために使用されます
  • T.Fatal(args ...interface{}) または T.Fatal(mst string, args interface{})T.Error() と似ていますがテストが失敗すると、それ以降のテストは実行されません。テストが失敗した時それ以降のテストも失敗する場合、 T.Fatal() を使うべきです

以下ではGoのテスト使用される2つのテクニックを紹介します。

モックとスタブにインターフェースを使用する

外部ライブラリに依存したコードを書いていて、その外部ライブラリが正しく利用されているかテストしたいときを考えます。

Goのインターフェースはメソッドの期待する動作を表しています。 例として io.Writer を見てみます。

type Writer interface {
    Write(p []byte) (n int, err error)
}

io.Writer インターフェースは引数で受け取ったバイト列を書き込みますが、このインターフェースは os.Fileなどで実装されています。Goのtypeシステムではどのインターフェースを使うか明示する必要がありません。既存のtypeのプロパティと一致するインターフェースを宣言することで、外部ライブラリの動作を変更することができます。

例を見ていきましょう。

以下のようなメッセージを送信する外部ライブラリがあります。

type Message struct {
     // ...
}

func (m *Message) Send(email, subject string, body []byte) error {
     // ...
     return nil
}

これをそのまま使うのではなくMessageを使うMessagerインターフェースを作成します。

type Messager interface {
    Send(email, subject string, body []byte) error
}

Alertメソッドでメッセージを送信することを考えます。Message typeを直接渡すのではなくMessager引数で受け取って、インターフェースのSendメソッドを呼び出すようにします。

func Alert(m Messager, problem []byte) error {
    return m.Send("example@example.com", "Critical Error", problem)
}

このようにMessageを抽象化したmessagerを使うことで簡単にモックを作成してテストすることができます。

具体的には以下のようになります。

package msg

import (
    "testing"
)

type MockMessage struct {
    email, subject  string
    body            []byte
}

func (m *MockMessage) Send(email, subject string, body []byte) error {
    m.email = email
    m.subject = subject
    m.body = body
    return nil
}

func TestAleart(t *testing.T) {
    msgr := new(MockMessage) // モックのメッセージを作成します
    body := []byte("Critical Error")

    Alert(msgr, body) // Aleartメソッドを実行します

    if msgr.subject != "Critical Error" {
        t.Errorf("Expected 'critical Error', Got '%s'", msgr.subject)
    }
}

Messagerインターフェースを実装するためにMockMessage typeを作成します。MockeMessageではMessagerと同じSend()が実装されています。このSend()はメーセージを実際に送信するのではなくデータをオブジェクトに保存しておくことでテストしやすくなります。

また、このようにインターフェースを使った抽象化をすることで、後にSend()の動作を変えなければいけなくなった時に簡単に変えられるようになります。

カナリアテスト

外部ライブラリを使っているとメジャーバージョンアップの時などにメソッドの引数が変わることがあります。

例えば、io.Writerを新しく実装していたとします。これをライブラリとして公開していて、他のコードがこれを使用しています。以下のようなコードです。

type MyWriter struct{
     // ...
}

func (m *MyWriter) Write([]byte) error {
     // どこかにデータを書き出す
     return nil
}

ぱっと見io.Writeを実装しているように見えますが、正しくはWrite(p []byte) (n int, err error)です。なのでio.Writeを実装できていません。

次に、type assertionを使ってコードを書いてみます。

func main() {
    m := map[string]interface{}{
        "w": &MyWriter(),
    }
}

func doSomething(m map[string]interface{}) {
    w := m["w"].(io.Writer) // runtime exceptionになる
}

このコードはコンパイルとは通りますが、runtimeでexceptionになります。

これを防ぐために以下のようなカナリアテストを追加します。(ちなみにカナリアテストは”canary in the coal mine”から来ているようです)

func TestWriter(t *testing.T) {
    var _ io.Writer = &MyWriter{} // コンパイラにtype assertionをやってもらう
}

このテストはもちろん失敗します。このようにtype assertionを使ってテストすることで、インターフェースを正しく実装できているか確認することができます。また、外部ライブラリのシグネチャの変更にも気づくことができます。

How to make development environment of Go with Mono and Protocol Buffers

I joined a project, and I begin to develop an application in it. This project has used golang. So I studied it and prepared its developing environment.

This article is the what I did to make development environment of GO.

Prerequisite

What our golang project needs.

Glide

Glide manage a project dependency.

What is the difference between “go get” and “Glide”

As far as I searched, “go get” just installs a package into your local directory, and solves library dependencies. By contrast, “Glide” is package manager sort of npm. It uses a file defined depending packages.

Fresh

Fresh is an auto-reloader for a web application. When you change a file of golang or template, Fresh reload (recompile) the file automatically, and apply the web application.

Goose

Goose is a migration tool.

Provision

Add provisioning config

We’ll provision from a shell script file.

Vagrantfile

Vagrant.configure("2") do |config|
  ...
  config.vm.provision :shell, path: "bootstrap.sh"
  ...
end

Environment variable golang uses

Golang uses GOROOT and GOPATH environment variables.

To installing a custom location use the GOROOT. If you don’t want change one, you don’t need to set this environment variable.

The GOPATH is used to specify directories for a golang project.

Install Go

Install Go and set GOROOT, GOPATH and PATH.

Install go and set environment variables for go

apt-get update
apt-get -y install curl git
wget https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz
tar -xvf go1.8.3.linux-amd64.tar.gz
mv go /usr/local

export GOROOT=/usr/local/go
export GOPATH=/vagrant/go
mkdir -p $GOPATH/bin
export PATH=$PATH:$GOPATH/bin:$GOROOT/bin
source ~/.bashrc

And You should add commands because GOROOT, GOPATH and PATH I set in bootstrap.sh doesn’t influence the vagrant environment.

The environment variables for golang in the vagrant environment

echo 'export GOROOT=/usr/local/go' >> /home/vagrant/.bashrc
echo 'export GOPATH=/vagrant/go' >> /home/vagrant/.bashrc
echo 'export PATH=$PATH:$GOPATH/bin:$GOROOT/bin' >> /home/vagrant/.bashrc

I install Glide.

Install Glide

curl https://glide.sh/get | sh

In the vagrant environment

Solve library dependency with Glide

It’s simple; you just execute above command.

Install glide

$ glide install

And install Fresh and Goose by go get.

Install Fresh and Goose

$ go get github.com/pilu/fresh
$ go get bitbucket.org/liamstask/goose/cmd/goose

Install protobuf

I referred to this article.

Install protobuf

$ curl -OL  https://github.com/google/protobuf/releases/download/v3.3.0/protoc-3.3.0-linux-x86_64.zip
$ unzip protoc-3.3.0-linux-x86_64.zip -d protoc3
$ sudo mv protoc3/bin/* /usr/local/bin/
$ sudo mv protoc3/include/* /usr/local/include/

Install Mono

I had to install an old version of Mono for the project reason. So I install it from tar ball.

This article is useful for me.

Install Mono

$ sudo apt-get install g++ gettext make
$ http://download.mono-project.com/sources/mono/mono-2.11.4.tar.bz2
$ tar xvjf mono-2.11.4.tar.bz2
$ cd mono-2.11.4
./configure --prefix=/opt/mono-2.11

$ make
$ make install

ln -s /opt/mono-2.11/bin/mono /usr/bin/mono
ln -s /opt/mono-2.11/bin/gmcs /usr/bin/gmcs