150 lines
9.5 KiB
Markdown
150 lines
9.5 KiB
Markdown
# 11.3 Goでどのようにテストを書くか
|
||
プログラムの開発においてテストはとても重要です。どのようにコードの質を保証するか、どのように各関数が実行できることを保証するか、また書いたコードの性能が良いことをどのように保証するかです。我々はユニットテストは主にプログラムの設計や実装のロジックエラーを発見することであると知っています。問題を早期に発見し、問題を特定し解決せしめ、性能をテストするにはプログラム設計上の問題のいくつかを発見することで、オンラインのプログラムがマルチプロセッシングしている状況でも安定を保てるようにします。この節ではこの一連の問題からGo言語でどのようにユニットテストと性能テストを実現するかご紹介します。
|
||
|
||
Go言語はあらかじめ用意されている軽量なテストフレームワーク`testing`と`go test`コマンドを使ってユニットテストと性能テストを実現します。`testing`フレームワークとその他の言語でのテストフレームワークはよく似ています。このフレームワークに基いて対応する関数に対してテストを書くことができます。またこのフレームワークに基づいて対応する耐久テストを書くこともできます。ではどのように書くのか一つ一つ見ていくことにしましょう。
|
||
|
||
## どのようにテストを書くか
|
||
`go test`コマンドでは対応するディレクトリ下の全てのファイルを実行するしかできません。そのため、`gotest`というディレクトリを新規に作成することで、すべてのコードとテストコードをこのディレクトリの中に配置することにします。
|
||
|
||
次にこのディレクトリの下に2つのファイルを新規に作成します:gotest.goとgotest_test.go
|
||
|
||
1. gotest.go:このファイルにはパッケージを一つ書きます。中身は除算を行う関数がひとつあります:
|
||
|
||
package gotest
|
||
|
||
import (
|
||
"errors"
|
||
)
|
||
|
||
func Division(a, b float64) (float64, error) {
|
||
if b == 0 {
|
||
return 0, errors.New("除数は0以外でなければなりません")
|
||
}
|
||
|
||
return a / b, nil
|
||
}
|
||
|
||
2. gotest_test.go:これはユニットテストのファイルですが、以下の原則を覚えておいてください:
|
||
|
||
- ファイル名は必ず`_test.go`が最後につくようにしてください。これによって`go test`を実行した時に対応するコードが実行されるようになります。
|
||
- `testing`というパッケージをimportする必要があります。
|
||
- すべてのテスト関数名は`Test`から始まります。
|
||
- テストはソースコードに書かれた順番に実行されます。
|
||
- テスト関数`TestXxx()`のパラメータは`testing.T`です。この型を使ってエラーやテストの状態を記録することができます。
|
||
- テストフォーマット: `func TestXxx (t *testing.T)`、`Xxx`の部分は任意の英数字の組み合わせです。ただし頭文字は小文字[a-z]ではいけません、例えば`Testintdiv`というのは間違った関数名です。
|
||
- 関数では`testing.T`の`Error`、`Errorf`、`FailNow`、`Fatal`、`FatalIf`メソッドをコールすることでテストがパスしないことを説明します。`Log`メソッドをコールすることでテストの情報を記録します。
|
||
|
||
以下は我々のテストコードです:
|
||
|
||
package gotest
|
||
|
||
import (
|
||
"testing"
|
||
)
|
||
|
||
func Test_Division_1(t *testing.T) {
|
||
if i, e := Division(6, 2); i != 3 || e != nil { //try a unit test on function
|
||
t.Error("除算関数のテストが通りません") // もし予定されたものでなければエラーを発生させます。
|
||
} else {
|
||
t.Log("はじめのテストがパスしました") //記録したい情報を記録します
|
||
}
|
||
}
|
||
|
||
func Test_Division_2(t *testing.T) {
|
||
t.Error("パスしません")
|
||
}
|
||
|
||
プロジェクトのディレクトリにおいて`go test`を実行すると以下のような情報が表示されます:
|
||
|
||
--- FAIL: Test_Division_2 (0.00 seconds)
|
||
gotest_test.go:16: パスしません
|
||
FAIL
|
||
exit status 1
|
||
FAIL gotest 0.013s
|
||
この結果が示すようにテストをパスしないのは、2つ目のテスト関数でテストが通らないコード`t.Error`を書いていたからです。では1つ目の関数が実行した状況はどうでしょうか?デフォルトでは`go test`を実行するとテストがパスする情報は表示されません。`go test -v`というオプションを追加する必要があります。このようにすると以下の情報が表示されます:
|
||
|
||
=== RUN Test_Division_1
|
||
--- PASS: Test_Division_1 (0.00 seconds)
|
||
gotest_test.go:11: 1つ目のテストがパス
|
||
=== RUN Test_Division_2
|
||
--- FAIL: Test_Division_2 (0.00 seconds)
|
||
gotest_test.go:16: パスしません
|
||
FAIL
|
||
exit status 1
|
||
FAIL gotest 0.012s
|
||
上の出力はこのテストのプロセスを詳細に表示しています。テスト関数1`Test_Division_1`ではテストが通りました。しかし関数2`Test_Division_2`のテストは失敗しました。最後にテストが通らないという結論を得ました。以降ではテスト関数2を以下のようなコードに修正します:
|
||
|
||
func Test_Division_2(t *testing.T) {
|
||
if _, e := Division(6, 0); e == nil { //try a unit test on function
|
||
t.Error("Division did not work as expected.") // 予期したものでなければエラーを発生
|
||
} else {
|
||
t.Log("one test passed.", e) //記録したい情報を記録
|
||
}
|
||
}
|
||
その後`go test -v`を実行すると以下のような情報を表示してテストがパスします:
|
||
|
||
=== RUN Test_Division_1
|
||
--- PASS: Test_Division_1 (0.00 seconds)
|
||
gotest_test.go:11: 1つ目のテストがパス
|
||
=== RUN Test_Division_2
|
||
--- PASS: Test_Division_2 (0.00 seconds)
|
||
gotest_test.go:20: one test passed. 除数は0以外
|
||
PASS
|
||
ok gotest 0.013s
|
||
|
||
## どのようにして耐久テストを書くか
|
||
耐久テストは関数(メソッド)の性能を測るために用いられます。ここでは再掲しませんが、ユニットテストを書くのと同じようなものです。ただし以下のいくつかに注意しなければなりません:
|
||
|
||
- 耐久テストは以下のループの形式で行われなければなりません。この中でXXXは任意の英数字の組み合わせです。ただし、頭文字は小文字ではいけません。
|
||
|
||
func BenchmarkXXX(b *testing.B) { ... }
|
||
|
||
- `go test`はデフォルトで耐久テストの関数を実行しません。もし耐久テストを実行したい場合はオプション`-test.bench`を追加します。文法:`-test.bench="test_name_regex"`。例えば`go test -test.bench=".*"`はすべての耐久テスト関数をテストすることを表します
|
||
- 耐久テストではテストが正常に実行されるよう、ループの中において`testing.B.N`を使用することを覚えておいてください
|
||
- ファイル名はかならず`_test.go`で終わります
|
||
|
||
以下ではwebbench_test.goという名前の耐久テストファイルを作成します。コードは以下の通り:
|
||
|
||
package gotest
|
||
|
||
import (
|
||
"testing"
|
||
)
|
||
|
||
func Benchmark_Division(b *testing.B) {
|
||
for i := 0; i < b.N; i++ { //use b.N for looping
|
||
Division(4, 5)
|
||
}
|
||
}
|
||
|
||
func Benchmark_TimeConsumingFunction(b *testing.B) {
|
||
b.StopTimer() //调用该函数停止压力测试的时间计数
|
||
|
||
//做一些初始化的工作,例如读取文件数据,数据库连接之类的,
|
||
//这样这些时间不影响我们测试函数本身的性能
|
||
|
||
b.StartTimer() //重新开始时间
|
||
for i := 0; i < b.N; i++ {
|
||
Division(4, 5)
|
||
}
|
||
}
|
||
|
||
|
||
`go test -file webbench_test.go -test.bench=".*"`というコマンドを実行すると、以下のような結果が現れます:
|
||
|
||
PASS
|
||
Benchmark_Division 500000000 7.76 ns/op
|
||
Benchmark_TimeConsumingFunction 500000000 7.80 ns/op
|
||
ok gotest 9.364s
|
||
|
||
上の結果は我々がどのような`TestXXX`なユニットテスト関数も実行していないことを示しています。表示される結果は耐久テスト関数のみを実行しただけです。第一行には`Benchmark_Division`が500000000回実行され示し、毎回の実行が平均で7.76ミリ秒であったことを示しています。第二行は`Benchmark_TimeConsumingFunctin`が500000000回実行され、毎回の平均実行時間が7.80ミリ秒であったことを示しています。最後の1行は全体の実行時間を示しています。
|
||
|
||
## まとめ
|
||
上のユニットテストと耐久テストの学習を通じて、`testing`パッケージが非常に軽量で、ユニットテストと耐久テストを書くのは非常に簡単であるとわかりました。ビルトインの`go test`コマンドを組み合わせることで、非常に便利にテストを行うことができます。このように我々が毎回コードを修正し終わる度に、go testを実行するだけで簡単に回帰テストを行うことができます。
|
||
|
||
|
||
## links
|
||
* [目次](<preface.md>)
|
||
* 前へ: [GDBを使用したデバッグ](<11.2.md>)
|
||
* 次へ: [まとめ](<11.4.md>)
|