Go言語における自動テスト
自動テストは現代のソフトウェア開発においては行って当然であり,
やるかやらないかではなく, いかにしてやるかだけが議論の的となるプラクティスといってよいでしょう.
多くの言語ではJavaのJUnitの子孫たちが移植され使われており, 自動テストのデファクトとなっている思われます.
Go言語では自動テストについても持ち前の自前主義を発揮し, サードパーティのライブラリではなく
ファーストパーティライブラリ, あるいは言語レベル(コンパイラレベル?)で機能を提供しています.
今回は, Go言語のユニットテスト機能について簡単に見ていきたいと思います.
なお, Go言語のテスト機能についての正式な名称が見当たらなかったので, 以下で必要な時はGoTestという名称を使います.
もっと適切な名称がありましたら, コメントで指摘お願いします.
基本的なルール
実行方法
テストの実行するには, 実行したいテストが含まれたディレクトリに移動し, 以下のようなコマンドを実行します.
> go test
このコマンドで, 現在のディレクトリに含まれているすべてのテスト関数が実行されます.
> go test
PASS
ok my.local/go-chat/client/src/mymath 0.003s
-v
オプションを追加することで, 実行された各テストの詳細情報を表示することもできます.
> go test -v
=== RUN TestIsEven
--- PASS: TestIsEven (0.00s)
=== RUN TestIsForenoon
--- PASS: TestIsForenoon (0.00s)
PASS
ok my.local/go-chat/client/src/mymath 0.003s
コマンドへの引数でテスト対象のディレクトリを指定することも可能ですが, その際にはGOPATH
からの相対パスを指定する必要があるようです.
> pwd
/home/vagrant/go/src/my.local/go-chat/client/src/mymath
> echo $GOPATH
/home/vagrant/go
> go test -v my.local/go-chat/client/src/mymath
=== RUN TestIsEven
--- PASS: TestIsEven (0.00s)
=== RUN TestIsForenoon
--- PASS: TestIsForenoon (0.00s)
PASS
ok my.local/go-chat/client/src/mymath 0.004s
>
テストファイルとテスト関数の作り方
JUnitがそうであったように, GoTestでもテスト対象となるファイルと関数は名前で判断します.
具体的には, GoTestは*_test.go
という名前のファイルに含まれたTestXxx
という名前の関数を
テスト関数とみなし実行します.
例えば, 以下のようなlib.go
ファイルを作った場合
package mymath;
func IsEven(i int) bool {
return i % 2 == 0;
}
これに対応するテストファイルとして, lib_test.go
ファイルを作る形になります.
package mymath
import "testing"
func TestIsEven(t *testing.Test) {
// ....
}
テスト関数の引数として受けとっている*testing.Test
型の変数はテストの失敗を記録するため構造体です.
このように作られたテストファイルは, 通常のビルド時(go build
)にはビルドの対象となりません.
アサーション
JUnitにおいては, テスト結果の比較用に各種アサーションメソッドが用意されていました.
しかしGoTestには, そのようなアサーション関数は用意されていません.
前節で少し書きましたが, テスト関数が受け取る*testing.Test
型の引数はあくまでテスト失敗時に結果を残す機能のみしか用意されておらず,
予想値と実測値の比較処理はすべて自分で記述する必要があります.
package mymath
import "testing"
func TestIsEven(t *testing.Test) {
if !IsEven(2) {
t.Error("2 was expected to be even number but it was odd number");
}
}
なぜJUnit等には用意されているアサート関数がGoTestには用意されていないのか?
それは早すぎる最適化を避けるというGo言語の思想によるものであり, アサート関数のような特定の状況でのみ有用な
ヘルパ関数は本当に必要になったときに初めて作るべきだからです. (と偉そうに書きましたがすべて伝聞です)
この思想の是非はさておき, ある程度テストが増え共通のパターンが見えてきたら, それに合わせてアサート関数を自作することは良いでしょう.
package mymath
import "testing"
func TestIsEven(t *testing.Test) {
var tests = []int { 4, 16, 36, 64, 81 };
for _, test := range tests {
assertEven(test, t);
}
}
func assertEven(i int, t *testing.Test) {
if !IsEven(i) {
t.Errorf("%d was expected to be even number but it was odd number", i);
}
}
応用的な使い方
特定のテスト関数だけ実行したい
プロジェクト初期のテストかまだ少ないころはテスト全体を一括で実行することに時別な問題はありませんが,
プロジェクトが進み, だんだんテストが増え, あるいは外部システム(DBとか)の低速な呼び出しを含むテストなどが作られてゆくと
テスト全体の実行に我慢できないほど時間がかかるようになることもよくあります.
GoTestでは, -run
オプションを使用し, 名前に指定した文字列を含むテスト関数のみを実行することができます.
> go test -v
=== RUN TestIsEven
--- PASS: TestIsEven (0.00s)
=== RUN TestIsForenoon
--- PASS: TestIsForenoon (0.00s)
PASS
ok my.local/go-chat/client/src/mymath 0.005s
> go test -v -run=Even
=== RUN TestIsEven
--- PASS: TestIsEven (0.00s)
PASS
ok my.local/go-chat/client/src/mymath 0.004s
あとはプロジェクトごとに
- 遅すぎて一括実行時には除外したいテストなどがある場合は, テスト関数名に
Show
の文字列を含めるようにする
などといったルールを決めていけば速度低下を軽減していくことができます.
Mock関数を利用する
これまで説明で使ってきた関数はそれ自体で処理が完結した単純なものでした.
しかし, 我々が現実に作らなければならない関数は自分自身だけでは完結せず, 他のパッケージによって提供されている関数に
依存していることが多いでしょう.
そのような依存関係のある関数に対してそのままテストを書くと, テストが失敗した際にその原因が対象の関数にあるのか
はたまたその依存先の関数にあるのか区別することが難しくなってしまいます.
以下のような関数を考えてみましょう.
package mymath
import "time";
// 現在が午前中かどうかを返す関数
func IsForenoon() {
var now := time.Now();
return now.Hour() < 12;
}
この関数は見ての通り現在時刻が午前か否かを返すだけの単純なものです.
しかし, 現在時刻を得るためにtime
パッケージのNow
関数を呼び出しているため, この関数に対するテストを書いても
実行する時間によっては成功したり失敗したとしてしまうことになります.
package mymath
import "testing"
func TestIsForenoon(t *testing.Test) {
if !IsForenoon() {
t.Error("now is Afternoon"); // <- 失敗するか否かはタイミング次第...
}
}
どうすれば実行タイミングによらず常に一定の結果を得られるテストが書けるようになるのでしょうか?
Javaなどのオブジェクト指向言語では, このような場合は機能を提供するオブジェクトを外部から抽入できるように設計し
テスト時は期待する挙動をするだけのモックオブジェクトに差し替えることで, テストの範囲を狭めています.
Go言語はオブジェクト指向言語ではなく, 関数もパッケージから直接importしているのでMockに差し替えることができないように思えます.
しかし, Go言語において関数はファーストクラスの値であり, 整数を変数に代入し再代入できるように
関数もまた変数に代入し再代入することができます.
当然その変数を関数として実行することも可能です.
なので以下のように, 上記の関数の外部依存部分の関数に__変数として__定義しておき
package mymath
import "time";
var now = func() : time.Time {
return time.Now();
}
// 現在が午前中かどうかを返す関数
func IsForenoon() {
var t := now();
return t.Hour() < 12;
}
テスト時には, now
変数を上書きすることで任意の時間に対してテストを行うことができます.
package mymath
import "time"
import "testing"
func TestIsForenoon(t *testing.Test) {
// オリジナル関数を一時退避
var original := now;
// テスト後にオリジナル関数を復元
defer func() { now = original; }();
var test := time.Date(2018, 12, 01, 11, 59, 59, 0, time.Local);
now = func() {
return test;
}
if !IsForenoon() {
t.Errorf("%s is Afternoon", test.String()); // <- テスト結果は常に一定
}
}
注意が必要なのは, 差し替えられた変数は自動でもとに戻ったりはしないので, 上述のようにdefer
構文を使って関数を復元する必要があります.
忘れぬよう.
テストカバレッジの作成
テストがどれだけのコードを実行されたかの統計情報を意味するテストカバレッジは
GoTestツールによって生成されるカバレッジファイルとGo言語が用意しているcover
ツールを使って作成します
まずGoTest実行時にcoverprofile
オプションで出力ファイルを指定してテストカバレッジファイルを作成します
> go test -v -coverprofile=covarege.o
PASS
coverage: 75.0% of statements
ok my.local/go-chat/client/src/mymath 0.004s
表示用のHTMLファイルへの変換はcover
ツールを使ってを行います.
> go tool cover -html=coverage.o
HTML output written to /tmp/cover518063326/coverage.html
最終的な結果は, 出来上がったHTMLをブラウザで表示することで確認するとこができます
何やら禍々しい色使いなのが気になりますが, コードのどの部分がテストされ, どの部分がテストされていないかを
確認することができます.
ベンチマーク
多くの言語ではベンチマークのためのツールとテストのためのツールは別物ですが, Go言語では両方の機能が
GoTestツールに統合されています.
ベンチマーク関数の作り方はテスト関数と似ていて, testing.B
型の引数を受け取るBenchmark
から始まる関数は自動的にベンチマーク関数と認識されます.
package mymath
import "time"
import "testing"
func BenchmarkIsForenoon(b *testing.B) {
for i := 0; i < b.N; i++ {
IsForenoon();
}
}
このように定義されたベンチマーク関数は, GoTestコマンドにbench
オプションを付けて実行することで実行することができます
> go test -bench=.
goos: linux
goarch: amd64
pkg: my.local/go-chat/client/src/mymath
BenchmarkIsForenoon-2 500000 3096 ns/op
PASS
ok my.local/go-chat/client/src/mymath 1.597s
bench
オプションには実行したいベンチマーク関数名のパターンを渡します.
すべてのベンチマーク関数を実行したい場合は「.」を指定します.
ベンチマーク関数で対象関数をN回実行し, その平均実行時間を求めます.
(N回が何回なのかは実行ごとにかわるようです?)
上の例では500000回実行され, 一回当たり3096nsかかったことがわかります.
各種オプションを指定することで, 実行時間以外の項目もベンチマークで調べることができます.
例えば, -benchmen
オプションを使うと, 使用メモリとmemory allocate回数を調べることができます.
詳しい情報はヘルプを調べるか, ドキュメントを調べてください
ドキュメント向けのサンプルコードを記述する
最後に, テストではありませんが, Go言語には自動生成されるドキュメント向けのサンプルコードを記述するルールも存在します.
この関数もテストと同じように, ExampleXxxx
(Xxxxはサンプルコードを記述したい関数名)という名前で定義することで
ドキュメント作成時に自動的に該当関数に関連づけられてサンプルコードが追記されます.
package mymath
func ExampleIsEven() {
fmt.PrintLn(IsEven(10));
fmt.PrintLn(IsEven(11));
// Output:
// true
// false
}
また, このExample
関数はテスト関数としてもみなされ, 上記のように最後にOutput:
と書かれたコメントが書かれていたら
fmt.Println
関数で標準出力に出力された結果と, コメントとして書かれた値の結果を比較して一致するかのテストが行われます.
> go test -v
=== RUN TestIsEven
--- PASS: TestIsEven (0.00s)
=== RUN TestIsForenoon
--- PASS: TestIsForenoon (0.00s)
=== RUN ExampleIsEven # <-- テストとして実行されている
--- PASS: ExampleIsEven (0.00s)
PASS
ok my.local/go-chat/client/src/mymath 0.006s
最後に
急ぎ足となりましたが, Go言語のテスト機能について私が理解している分の解説はこれで終わります.
testing
パッケージにはまだまだいろいろな機能があるようなので, より詳しく知りたい方はオンラインドキュメントを調べたり
書籍を読んでみたりなどしてみてください.