Goでユニットテストを書く方法
著者は、 Write for DOnations プログラムの一環として、 FreeBSDFoundationを選択して寄付を受け取りました。
序章
単体テストは、プログラムまたはパッケージから特定のコードをテストする関数です。 単体テストの仕事はアプリケーションの正しさをチェックすることであり、それらはGoプログラミング言語の重要な部分です。
このチュートリアルでは、小さなプログラムを作成し、Goのテストパッケージとgo testコマンドを使用して、コードで一連のテストを実行します。 チュートリアルを完了すると、テーブルベースの単体テスト、カバレッジテスト、ベンチマーク、および文書化された例。
前提条件
このチュートリアルを完了するには、次のものが必要です。
-
Goプログラミング言語に精通していること。 言語の幅広い紹介については、チュートリアルシリーズ/電子ブックGoでコーディングする方法をご覧ください。
-
Goバージョン1.11以降がローカルマシンにインストールされています。 これらの手順に従って、Goを Linux 、 macOS 、およびWindowsにインストールできます。 macOSでは、Homebrewパッケージマネージャーを使用してGoをインストールすることもできます。
注:このチュートリアルでは、Goバージョン1.11で導入されたパッケージ管理システムであるGoモジュールを使用します。 Goモジュールは、$ GOPATHを置き換えることを目的としており、Goバージョン1.13以降のデフォルトオプションになりました。 Goモジュールと$GOPATHの違いのより包括的な概要については、Goコアチームからのこの公式ブログ投稿を読むことを検討してください。
このチュートリアルは、Goバージョン1.14を使用してテストされました
ステップ1—単体テスト用のサンプルプログラムを作成する
単体テストを作成する前に、テストで分析するためのコードが必要です。 このステップでは、2つの整数を合計する小さなプログラムを作成します。 以降の手順では、go test
を使用してプログラムをテストします。
まず、math
という名前の新しいディレクトリを作成します。
- mkdir ./math
新しいディレクトリ内に移動します。
- cd ./math
これがプログラムのルートディレクトリになり、残りのすべてのコマンドをここから実行します。
次に、nano
またはお好みのテキストエディタを使用して、math.go
という名前の新しいファイルを作成します。
- nano math.go
次のコードを追加します。
package math
// Add is our function that sums two integers
func Add(x, y int) (res int) {
return x + y
}
// Subtract subtracts two integers
func Subtract(x, y int) (res int) {
return x - y
}
ここでは、Add
とSubtract
という2つの関数を作成しています。 各関数は2つの整数を受け入れ、それらの合計(func Add
)またはそれらの差(func Subtract
)のいずれかを返します。
ファイルを保存して閉じます。
このステップでは、Goでコードを記述しました。 次に、次の手順で、コードが正しく機能することを確認するための単体テストを作成します。
ステップ2—Goでユニットテストを書く
このステップでは、Goで最初のテストを作成します。 Goでテストを作成するには、テストファイルのリンクが必要です。このテストファイルは、常に_test.go
で終わる必要があります。 慣例により、Goテストファイルは常に、テストしているコードが存在する同じフォルダーまたはパッケージに配置されます。 これらのファイルは、go build
コマンドを実行したときにコンパイラーによってビルドされないため、デプロイメントで終了することを心配する必要はありません。
そして、Goのすべてと同様に、言語はテストについて意見が分かれています。 Go言語は、開発者がgo test
コマンドと一緒に使用するtestingと呼ばれる最小限でありながら完全なパッケージを提供します。 testing
パッケージは、カバレッジテストやベンチマークなど、いくつかの便利な規則を提供します。
エディタを使用して、math_test.go
という名前の新しいファイルを作成して開きます。
- nano math_test.go
Goのテスト関数には、func TestXxxx(t *testing.T)
というシグネチャが含まれています。 つまり、すべてのテスト関数はTest
という単語で始まり、その後に最初の単語が大文字になっている接尾辞が続く必要があります。 Goのテスト関数は1つのパラメーターのみを受け取ります。この場合、それはタイプtesting.T
のポインターです。 このタイプには、t.Errorf()
メソッドのように、結果を出力し、エラーを画面に記録し、障害を通知するために必要な便利なメソッドが含まれています。
次のコードをmath_test.go
に追加します。
package math
import "testing"
func TestAdd(t *testing.T){
got := Add(4, 6)
want := 10
if got != want {
t.Errorf("got %q, wanted %q", got, want)
}
}
まず、テストするパッケージの名前math
を宣言します。 次に、testing
パッケージ自体をインポートします。これにより、testing.T
タイプと、パッケージによってエクスポートされた他のタイプおよびメソッドが使用可能になります。 コードとテストロジックは、TestAdd
関数に含まれています。
要約すると、Goでのテストの特徴は次のとおりです。
- 最初で唯一のパラメータは
t *testing.T
でなければなりません - テスト関数は、単語
Test
で始まり、大文字で始まる単語または句が続きます(慣例では、テスト対象のメソッドの名前を使用します(例:TestAdd
))。 - テストでは、
t.Error
またはt.Fail
を呼び出して、失敗を示します(t.Fail
よりも詳細が返されるため、t.Error
を呼び出しています)。 t.Log
を使用して、失敗しないデバッグ情報を提供できます- テストは、
math_test.go
などのfoo_test.go
という命名規則を使用してファイルに保存されます。
ファイルを保存して閉じます。
このステップでは、Goで最初のテストを作成しました。 次のステップでは、go test
を使用してコードをテストします。
ステップ3—go test
コマンドを使用してGoコードをテストする
このステップでは、コードをテストします。 go test
は、テストの自動化に役立つ強力なサブコマンドです。 go test
は、実行するテスト、テストが返す冗長性などを構成できるさまざまなフラグを受け入れます。
プロジェクトのルートディレクトリから、最初のテストを実行します。
- go test
次の出力が表示されます。
OutputPASS
ok ./math 0.988s
PASS
は、コードが期待どおりに機能していることを意味します。 テストが失敗すると、FAIL
が表示されます。
go test
サブコマンドは、_test.go
サフィックスが付いたファイルのみを検索します。 次に、go test
は、これらのファイルをスキャンして、func TestXxx
や、後の手順で説明するその他のいくつかの特殊機能を探します。 go test
は、これらの関数を適切な方法で呼び出し、ビルドして実行し、結果を報告し、最後にすべてをクリーンアップする一時的なメインパッケージを生成します。
私たちのgo test
はおそらく私たちの小さなプログラムには十分ですが、実行中のテストとそれぞれにかかる時間を確認したい場合があります。 -v
フラグを追加すると、冗長性が高まります。 新しいフラグを使用してテストを再実行します。
- go test -v
次の出力が表示されます。
Output=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok ./math 1.410s
このステップでは、go test
サブコマンドを使用して基本的な単体テストを実行しました。 次のステップでは、より複雑なテーブル駆動の単体テストを作成します。
ステップ4—Goでテーブル駆動型テストを作成する
テーブル駆動テストは、さまざまな値と結果のテーブルを維持することを除けば、基本的な単体テストに似ています。 テストスイートはこれらの値を繰り返し処理し、テストコードに送信します。 このアプローチを使用して、入力とそれぞれの出力のいくつかの組み合わせをテストすることができます。
ここで、単体テストを構造体のテーブルに置き換えます。このテーブルのフィールドには、Add
関数に必要な2つの引数(2つの整数)と期待される結果(それらの合計)が含まれます。
math_test.go
を再度開きます:
- nano math_test.go
ファイル内のすべてのコードを削除し、代わりに次のテーブル駆動型単体テストを追加します。
package math
import "testing"
// arg1 means argument 1 and arg2 means argument 2, and the expected stands for the 'result we expect'
type addTest struct {
arg1, arg2, expected int
}
var addTests = []addTest{
addTest{2, 3, 5},
addTest{4, 8, 12},
addTest{6, 9, 15},
addTest{3, 10, 13},
}
func TestAdd(t *testing.T){
for _, test := range addTests{
if output := Add(test.arg1, test.arg2); output != test.expected {
t.Errorf("Output %q not equal to expected %q", output, test.expected)
}
}
}
ここでは、構造体を定義し、Add
関数の引数と期待される結果を含む構造体のテーブルにデータを入力してから、新しいTestAdd
関数を記述しています。 この新しい関数では、テーブルを反復処理し、引数を実行し、出力を期待される各結果と比較して、エラーが発生した場合はそれを返します。
ファイルを保存して閉じます。
次に、-v
フラグを使用してテストを実行します。
- go test -v
以前と同じ出力が表示されます。
Output=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok ./math 1.712s
ループが繰り返されるたびに、コードはAdd
関数によって計算された値を期待値と照合してテストします。
このステップでは、テーブル駆動テストを作成しました。 次のステップでは、代わりにカバレッジテストを作成します。
ステップ5—Goでカバレッジテストを作成する
このステップでは、Goでカバレッジテストを記述します。 テストを作成するときは、テストがカバーする実際のコードの量を知ることが重要になることがよくあります。 これは一般にカバレッジと呼ばれます。 これが、Subtract
関数のテストを作成していない理由でもあります。そのため、不完全なカバレッジテストを表示できます。
次のコマンドを実行して、現在の単体テストのカバレッジを計算します。
- go test -coverprofile=coverage.out
次の出力が表示されます。
OutputPASS
coverage: 50.0% of statements
ok ./math 2.073s
このカバレッジデータをファイルcoverage.out
に保存します。 これで、結果をWebブラウザに表示できます。
次のコマンドを実行します。
- go tool cover -html=coverage.out
Webブラウザーが開き、結果が次のように表示されます。
緑のテキストはカバレッジを示し、赤のテキストはその反対を示します。
このステップでは、テーブル駆動の単体テストのカバレッジをテストしました。 次のステップでは、関数のベンチマークを行います。
ステップ6—Goでベンチマークを書く
このステップでは、Goでベンチマークテストを作成します。 ベンチマークは、機能またはプログラムのパフォーマンスを測定します。 これにより、実装を比較し、コードに加えた変更の影響を理解できます。 その情報を使用して、最適化する価値のあるGoソースコードの一部を明らかにすることができます。
Goでは、func BenchmarkXxx(*testing.B)
の形式の関数がベンチマークと見なされます。 go test
は、-bench
フラグを指定すると、これらのベンチマークを実行します。 ベンチマークは順番に実行されます。
単体テストにベンチマークを追加しましょう。
math_test.go
を開きます:
- nano math_test.go
次に、func BenchmarkXxx(*testing.B)
構文を使用してbenchamrk関数を追加します。
...
func BenchmarkAdd(b *testing.B){
for i :=0; i < b.N ; i++{
Add(4, 6)
}
}
ベンチマーク関数は、ターゲットコードをbN回実行する必要があります。ここで、Nは調整可能な整数です。 ベンチマークの実行中、bNは、ベンチマーク機能が確実にタイミングをとれるのに十分長く続くまで調整されます。 --bench
フラグは、正規表現の形式で引数を受け入れます。
ファイルを保存して閉じます。
次に、go test
をもう一度使用してベンチマークを実行してみましょう。
- go test -bench=.
.
は、ファイル内のすべてのベンチマーク関数と一致します。
ベンチマーク関数を明示的に宣言することもできます。
- go test -bench=Add
いずれかのコマンドを実行すると、次のような出力が表示されます。
Outputgoos: windows
goarch: amd64
pkg: math
BenchmarkAdd-4 1000000000 1.07 ns/op
PASS
ok ./math 2.074s
結果の出力は、ループがループあたり1.07ナノ秒の速度で10,000,000回実行されたことを意味します。
注:他の目的で使用されているビジー状態のシステムでGoコードのベンチマークを行わないようにしてください。ベンチマークプロセスに干渉し、不正確な結果が得られます。
これで、成長する単体テストにベンチマークが追加されました。 次の最後のステップでは、ドキュメントに例を追加します。これはgo test
でも評価されます。
ステップ7—例を使用してGoコードを文書化する
このステップでは、Goコードを例とともに文書化し、それらの例をテストします。 Goは適切なドキュメントに非常に重点を置いており、サンプルコードはドキュメントとテストの両方に別の側面を追加します。 例は、既存のメソッドと関数に基づいています。 例では、特定のコードの使用方法をユーザーに示す必要があります。 関数の例は、go test
サブコマンドによって特別に処理される3番目のタイプの関数です。
開始するには、math_test.go
を再度開き、
- nano math_test.go
次に、強調表示されたコードを追加します。 これにより、 fmtパッケージがインポートリストに追加され、ファイルの最後にサンプル関数が追加されます。
package math
import (
"fmt"
"testing"
)
// arg1 means argument 1 and arg2 means argument 2, and the expected stands for the 'result we expect'
type addTest struct {
arg1, arg2, expected int
}
var addTests = []addTest{
addTest{2, 3, 5},
addTest{4, 8, 12},
addTest{6, 9, 15},
addTest{3, 10, 13},
}
func TestAdd(t *testing.T) {
for _, test := range addTests {
if output := Add(test.arg1, test.arg2); output != test.expected {
t.Errorf("Output %q not equal to expected %q", output, test.expected)
}
}
}
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(4, 6)
}
}
func ExampleAdd() {
fmt.Println(Add(4, 6))
// Output: 10
}
Output:
行は、期待される出力を指定して文書化するために使用されます。
注:比較では、先頭と末尾のスペースは無視されます。
ファイルを保存して閉じます。
次に、単体テストを再実行します。
- go test -v
次のような更新された出力が表示されます。
Output=== RUN TestAdd
--- PASS: TestAdd (0.00s)
=== RUN ExampleAdd
--- PASS: ExampleAdd (0.00s)
PASS
ok ./math 0.442s
これで、例もテストされました。 この機能により、ドキュメントが改善され、単体テストがより堅牢になります。
結論
このチュートリアルでは、小さなプログラムを作成してから、その機能を確認するための基本的な単体テストを作成しました。 次に、単体テストをテーブルベースの単体テストとして書き直し、カバレッジテスト、ベンチマーク、および文書化された例を追加しました。
適切な単体テストを作成するために時間をかけることは、作成したコードまたはプログラムが期待どおりに機能し続けるという確信を高めるため、プログラマーとして役立ちます。 Goのtesting
パッケージは、かなりの単体テスト機能を提供します。 詳細については、Goの公式ドキュメントを参照してください。
また、Goでのプログラミングについて詳しく知りたい場合は、チュートリアルシリーズ/無料の電子書籍、Goでのコーディング方法にアクセスしてください。