📝

dockerコンテナ内でgoのテストをやってみた

2020/10/23に公開

dockerコンテナ上のgo実行環境でユニットテストをやってみました。テスト自体を書くのが初めてだったのでしょぼいものになりそうですがよろしくお願いします。

プログラミングにおけるテストとは

私が初めてプログラミングのテストについて聞いたのはCI/CDの文脈の中でした。「持続的なインテグレーション・デプロイのためにテストを自動化すると良い。」というコンテキストの中で語られたので何かすごいことをするものだと思ってしまい、そこから存在は知っているけど隅においている状態でした。

いつまでも逃げていても仕方ないと思い最近になって少し調べ始めたらそんな難しいものではないことに気付きました。

もしかしたらみんなやったことあるかも

プログラミングにおけるテストは自分の書いたコードが自分の意図した通りに動くかどうかを確認することです。

例えば私はこれまでにチャットアプリを作ったことがありますが、そこで作ったログインのシステムがちゃんと動作するかチェックするために実際にIDとパスワードを入力して確認していました。
他にもゲーム業界だとフィールドの壁という壁にぶつかり続けてすり抜ける場所がないかをチェックするバイトがありますがこれもテストになるでしょう(今もそういうバイトあるのかな?)。

効率的にテストするためにテストプログラムを書く

つまりコードを書く必要はありません。しかしプログラムがしっかり意図した通り動いているかを確認するにはたくさんのパターンでテストをする必要があります。
例えばさっきのログイン処理でもパスワードは半角英数で8〜16文字で数字と英語を織り交ぜる必要があるなどいろいろ制約があるでしょう。そうすると制約を守っていない入力をちゃんとはじくかを確認するために7文字で入力してみたり、20文字で入力したり、数字だけで入力したりいくつものパターンをやる必要があるわけです。またプログラムに修正を加える度に同じテストを手動でやるのはとても煩雑です。
なのでそのテストもプログラムでやってしまおうということでテストプログラムを書きます。また、テストプログラムはプログラムの挙動をテストするための記述なのでプログラムの仕組みを作成していない人に伝える説明の役割も担ったりします。

今回はそのテストプログラムを書いてみようという記事です。

テストを作ってみる

ソフトウェアなどは大量のプログラムの集まりになるので大抵の場合機能ごとに切り分けて作ります。
ここでのテストは切り分けた機能がちゃんと意図した挙動をするかを確認するテストとそれを組み合わせてソフトウェアにした時に意図した挙動をするかを確認するテストに分けて考えます。このように分けることで変な要素を考慮にいれる必要がなくシンプルにテストを行えます。
前者をユニットテストといい、後者をインテグレーションテストとかジョイントテストと言ったりします。

今回はデータベースにデータを書き込む以下の関数についてテストを書いてみます。関数とは一つの機能なのでユニットテストになります。

db.go
type StringLengthError struct {
	text string
}

func (e *StringLengthError) Error() string {
	return fmt.Sprintf("text %v length over 140 or 0.", e.text)
}
// PostTalk データベースにトークを投稿する。1~140文字の間
func PostTalk(t string) error {
	if len(t) < 0 || len(t) > 140 {
		return &StringLengthError{t}
	}
	db, err := sql.Open(dName, dsName)
	if err != nil {
		return err
	}
	defer db.Close()
	ins, err := db.Prepare("INSERT INTO talks(talk,create_at,update_at) VALUES(?,NOW(),NOW())")
	if err != nil {
		return err
	}
	ins.Exec(t)
	return nil
}

コードをみるとわかるように今回は1~140文字の間の文のみを書き込むようにしているのでそこをチェックする必要があります。なので以下のようなテストを書きます。

db_test.go
package db

import "testing"
// ①
type PostTalkTestValue struct {
	value  string // テストで流す値
	result bool   // 値に対してエラーがあったかどうか。なし:false, あり:true
}
// ②
func TestPostTalk(t *testing.T) {
	// ③
	tests := []PostTalkTestValue{
		{value: "こんにちは", result: false},
		// 140文字以上
		{value: "あああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああ", result: true},
		{value: "", result: true},
	}
	// ④
	for i, test := range tests {
		err := PostTalk(test.value)
		if (err != nil) != test.result {
			t.Errorf("No. %v test failed. input value: %v want result: %v", i+1, test.value, test.result)
		}
	}
}

Goでは**_test.goというファイルを作ることで **.goのテストを作ることが出来ます。テストではtestingパッケージをインポートします。

  1. 今回はテストを行う関数に流し込む値valueと返り値のエラーの有無をチェックするresultをまとめたPostTalkTestValueという構造体を作りテストに使います。
  2. テストを行う関数を宣言します。
  3. 一つだけの値でチェックしても意味がないので、複数の値を流しこむために配列でtests変数を定義します。
  4. forループで変数testsの一つ一つの要素に対してテストを行います。

実行結果

/go/src/app/mod/db # go test
--- FAIL: TestPostTalk (0.07s)
    db_test.go:19: No. 3 test failed. input value:  want result: true
FAIL
exit status 1
FAIL    app/mod/db      0.080s

結果をみてみると3つ目のテストで失敗していることがわかります。これは空の文字列を入力しているので文字列の長さをチェックする記述に問題がありそうです。db.goのコードを見直してみると本来はlen(t) <= 0でなければいけないのにlen(t) < 0になっていますね。これを修正しましょう。
また、失敗しかログが出ていないので成功した時もログが出るようにテストも書き換えましょう。
修正

db_test.go
func TestPostTalk(t *testing.T) {
	tests := []PostTalkTestValue{
		{value: "こんにちは", result: false},
		// 140文字以上
		{value: "あああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああ", result: true},
		{value: "", result: true},
	}
	for i, test := range tests {
		err := PostTalk(test.value)
		if (err != nil) != test.result {
			t.Errorf("No. %v test failed. input value: %v want result: %v", i+1, test.value, test.result)
		} else {
			if err != nil {
				fmt.Println(err)
			} else {
				fmt.Println("write db success")
			}
		}
	}
}

実行結果

/go/src/app/mod/db # go test
write db success
text あああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああ length over 140 or 0.
text  length over 140 or 0.
PASS

実行結果からテストはパスしたのでこちらが想定した結果が得られるようです。

パスが通ってない!?

ちなみにコンテナ上でGoのテストを実行すると以下のようなエラーが出てくるかもしれません。

/go/src/app/mod/db # go test
# runtime/cgo
exec: "gcc": executable file not found in $PATH
FAIL    app/mod/db [build failed]

その場合は以下のようにgo envの設定を変更してあげてください。

/go/src/app/mod/db # export CGO_ENABLED=0

せっかくなのでインストールしたgotestsでテストファイルを自動生成してみよう

gotestsではファイル・パッケージ・関数などいろいろなスケールでテストが出来ます。今回は先ほどと同様に単体の関数をテストしてみます。
テストをしたい関数のところにカーソルを合わせて、
command + shift + Pで入力を開き、
Go: Generate Unit Tests For Function
を選ぶと同じディレクトリにdb_test.goが以下のように自動生成されます。

db/db_test.go
package db

import (
	"testing"

	_ "github.com/go-sql-driver/mysql"
)

func TestPostTalk(t *testing.T) {
	type args struct {
		t string
	}
	tests := []struct {
		name    string
		args    args
		wantErr bool
	}{
		// TODO: Add test cases.
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if err := PostTalk(tt.args.t); (err != nil) != tt.wantErr {
				t.Errorf("PostTalk() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

関数を解析してテストを自動生成してくれます。以下のように// TODO: Add test cases.と書いてある部分に流し込むデータなどを定義してあげれば完成なので簡単にテストを作ることが出来ます。

  • name: テストの名前
  • args: 関数に流し込むデータ
  • watErr: 今回は帰ってくるエラーが妥当かどうかをチェックすれば良いので流し込んだデータに対してエラーが帰ってくるかどうかをブールで設定する。
db/db_test.go
package db

import (
	"testing"

	_ "github.com/go-sql-driver/mysql"
)

func TestPostTalk(t *testing.T) {
	type args struct {
		t string
	}
	tests := []struct {
		name    string
		args    args
		wantErr bool
	}{
		// TODO: Add test cases.
		{name: "書き込み1", args: args{t: "こんにちは"}, wantErr: false},
		{name: "書き込み2", args: args{t: "ああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああ"}, wantErr: true},
		{name: "書き込み3", args: args{t: ""}, wantErr: true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if err := PostTalk(tt.args.t); (err != nil) != tt.wantErr {
				t.Errorf("PostTalk() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

実行結果

/go/src/app/mod/db # go test
PASS
ok      app/mod/db      0.020s

簡単なテストならgotestsで自動生成すると楽出来そうですね。

Discussion