Go言語でテストコードを書いてみよう
こんにちは。株式会社レスキューナウの新プロダクトチームで主にバックエンドを担当している本田です。
現在、私のチームではバックエンドにGo言語を採用し、開発を進めています。
プロダクト開発を進めるうえで、どんな言語でもテストは欠かせません。
そういうわけで、今回は「Go言語のテストコードを書いてみよう」と題して、テストコードの書き方やTipsをご紹介したいと思います。
テストコードの書き方
Go言語のテストは testingパッケージ を利用します。
ファイル名と関数名の命名には下記の決まりがあります。
テストファイル名: xxx_test.go
テスト関数名: TestXxxもしくは Test_xxx(TestxxxはNG)
xxx_test.go
import "testing"
func TestXxx(*testing.T){
}
テストファイルの場所
テスト対象のファイルとテストファイルは同じディレクトリに配置するのを推奨されています。※別のフォルダに置くとカバレッジの取得ができなくなります。
sample_dir
├── xxx.go
└── xxx_test.go
テストの実行方法
テストの実行は go test ./dirctory… で実行できます。 "…"をつけるとdirctory配下のディレクトリ下にあるテストファイルも実行します。
$ go test ./...
また、さまざまなオプションを指定することができます。
「-v」は詳細も表示する。
「--short」はフラグのあるテストファイルをskipする。(※後述)
「-cover」はカバレッジの取得ができます。(※後述)
テストコードを書いてみよう
では実際にテストコードを書いてみましょう。
サンプルコードを元にテストコードを書いていきます。
サンプルコードの作成
sample/
├── go.mod
├── handler
│ ├── user_handler.go
│ └── user_handler_test.go
└── main.go
handler/user_handler.go
1 package handler
2
3 import "fmt"
4
5 type User struct {
6 Name string
7 Age int
8 }
9
10 func (u *User) GetMessage(message string) string {
11 return fmt.Sprintf("%s(%d)さん, %s", u.Name, u.Age, message)
12 }
sample/main.go
1 package main
2
3 import (
4 "fmt"
5 "sample/handler"
6 )
7
8 func main() {
9 user1 := handler.User{"山田", 22}
10 fmt.Println(user1.GetMessage("こんちには"))
11
12 user2 := handler.User{"佐藤", 26}
13 fmt.Println(user2.GetMessage("はじめまして"))
14 }
実行結果
山田(22)さん, こんちには
佐藤(26)さん, はじめまして
サンプルコードは、User構造体を生成して、GetMessageメソッドで"名前(年齢)、メッセージ"を表示させるだけのプログラムです。
テストコードの生成
テストコードは0から書くのは手間ですが、VSCodeやGolandなどのIDEではひな形を生成してくれるので非常にラクです。
Golandの場合だと元ファイル上で「Command + N」を押すと下記のようなポップアップが出てくるので、「Tests for file」を選ぶとテストコードのひな形が生成されます。
生成されたひな形
handler/user_handler_test.go
1 package handler
2
3 import "testing"
4
5 func TestUser_GetMessage(t *testing.T) {
6 type fields struct {
7 Name string
8 Age int
9 }
10 type args struct {
11 message string
12 }
13 tests := []struct {
14 name string
15 fields fields
16 args args
17 want string
18 }{
19 // TODO: Add test cases.
20 }
21 for _, tt := range tests {
22 t.Run(tt.name, func(t *testing.T) {
23 u := &User{
24 Name: tt.fields.Name,
25 Age: tt.fields.Age,
26 }
27 if got := u.GetMessage(tt.args.message); got != tt.want {
28 t.Errorf("GetMessage() = %v, want %v", got, tt.want)
29 }
30 })
31 }
32 }
生成されたひな形を元に作成したテストコードが下記になります。
1 package handler
2
3 import "testing"
4
5 func TestUser_GetMessage(t *testing.T) {
6 type fields struct {
7 Name string
8 Age int
9 }
10 type args struct {
11 message string
12 }
13 tests := []struct {
14 name string
15 fields fields
16 args args
17 want string
18 }{
19 {"テスト1", fields{"山田", 22}, args{"こんにちは"}, "山田(22)さん, こんにちは"},
20 {"テスト2", fields{"佐藤", 26}, args{"はじめまして"}, "佐藤(26)さん, こんにちは"},
21 }
22 for _, tt := range tests {
23 t.Run(tt.name, func(t *testing.T) {
24 u := &User{
25 Name: tt.fields.Name,
26 Age: tt.fields.Age,
27 }
28 if got := u.GetMessage(tt.args.message); got != tt.want {
29 t.Errorf("GetMessage() = %v, want %v", got, tt.want)
30 }
31 })
32 }
33 }
今回は
テスト1:
入力:User.Nameが”山田”、User.Ageが"22"、messageが”こんにちは”
期待する結果:"山田(22)さん, こんにちは"
テスト2:
入力:User.Nameが”佐藤”、User.Ageが"26"、messageが”はじめまして”
期待する結果:"佐藤(26)さん, はじめまして"
の2通りのテストを実行します。
テスト実行
それでは実際にテスト実行してみましょう。
$ go test ./... -v
? sample [no test files]
=== RUN TestUser_GetMessage
=== RUN TestUser_GetMessage/テスト1
=== RUN TestUser_GetMessage/テスト2
--- PASS: TestUser_GetMessage (0.00s)
--- PASS: TestUser_GetMessage/テスト1 (0.00s)
--- PASS: TestUser_GetMessage/テスト2 (0.00s)
PASS
ok sample/handler 0.194s
テスト1もテスト2 も問題なく通りました。
これだけだと物足りないので、テストケースを変えて、あえて失敗するようにしてみます。
12 }
13 tests := []struct {
14 name string
15 fields fields
16 args args
17 want string
18 }{
19 {"テスト1", fields{"山田", 22}, args{"こんにちは"}, "山田(22)さん, こんにちは"},
20 {"テスト2", fields{"佐藤", 26}, args{"はじめまして"}, "佐藤(26)さん, こんにちは"},
21 }
$ go test ./... -v
? sample [no test files]
=== RUN TestUser_GetMessage
=== RUN TestUser_GetMessage/テスト1
=== RUN TestUser_GetMessage/テスト2
user_handler_test.go:29: GetMessage() = 佐藤(26)さん, はじめまして, want 佐藤(26)さん, こんにちは
--- FAIL: TestUser_GetMessage (0.00s)
--- PASS: TestUser_GetMessage/テスト1 (0.00s)
--- FAIL: TestUser_GetMessage/テスト2 (0.00s)
FAIL
FAIL sample/handler 0.200s
FAIL
テスト2の期待する結果を"佐藤(26)さん, こんにちは"に変更してみました。
実際に出力されるのは"佐藤(26)さん, はじめまして"なので、テストが失敗していることが分かりますね。
テスト時のログを出力したい
テストコードの合間で状態を確認したい時があると思います。
testingパッケージでは、t.Log()やt.Logf()関数でログを出力できます。
ログ出力にはlog.Println()やlog.Printf()関数がありますが、下記の違いから、t.Log()やt.Logf()を使う方が便利です。
log.Println()/log.Printf():失敗したテストだけでなく、成功したテストもログが出力される。
t.Log()/t.Logf(): 失敗したテストのみログが出力される。
22 for _, tt := range tests {
23 t.Run(tt.name, func(t *testing.T) {
24 log.Printf("log.Printf %s \n", tt.name)
25
26 u := &User{
27 Name: tt.fields.Name,
$ go test ./...
? sample [no test files]
2022/09/27 12:01:04 log.Printf テスト1 //成功したテスト1も出力される
2022/09/27 12:01:04 log.Printf テスト2
--- FAIL: TestUser_GetMessage (0.00s)
--- FAIL: TestUser_GetMessage/テスト2 (0.00s)
user_handler_test.go:40: GetMessage() = 佐藤(26)さん, こんにちは, want 佐藤(26)さん, はじめまして
FAIL
FAIL sample/handler 0.188s
FAIL
log.Printf()の場合、成功したテスト1のログも表示されます。
22 for _, tt := range tests {
23 t.Run(tt.name, func(t *testing.T) {
24 t.Logf("t.Logf %s \n", tt.name)
25
26 u := &User{
27 Name: tt.fields.Name,
$ go test ./...
? sample [no test files]
--- FAIL: TestUser_GetMessage (0.00s)
--- FAIL: TestUser_GetMessage/テスト2 (0.00s)
user_handler_test.go:31: t.Logf テスト2 //失敗したテスト2のみ出力される
user_handler_test.go:39: GetMessage() = 佐藤(26)さん, こんにちは, want 佐藤(26)さん, はじめまして
FAIL
FAIL sample/handler 0.198s
FAIL
t.Logf()の場合、失敗したテスト2のログだけが出力されます。
さらに行番号も表示されます。
失敗したテストのログのみを出力できるt.Log()やt.Logf()の方が使い勝手は良さそうです。
テストの失敗時に終了させたい、もしくは継続させたい
テストの失敗時に以降の処理を実行せず強制終了させたい場合は、t.FailNow()もしくはt.Fatalf()を使います。両者の違いは、t.Fatalf()はログを出力できます。
反対にテストが失敗しても処理を継続させたい場合は、t.Fail()もしくはt.Errorf()を使います。t.Errorf()はログを出力できます。
テストのキャッシュを削除したい
$ go test ./... -v
....
--- PASS: TestUser_GetMessage/テスト2 (0.00s)
PASS
ok sample/handler (cached) ←//キャッシュ
テストを行っていると、cachedの文字が表示されることがあります。前回のテストから対象のファイルやテストコードに変更がない場合は、キャッシュを利用するためです。テストのキャッシュについてはこちらのドキュメントで詳しく説明されています。
$ go test ./... -v -count=1
$ go clean -testcache
キャッシュを使わない場合はオプションで「-count=1」を指定します。
また、「go clean -testcache」でキャッシュを削除することもできます。
テストをスキップしたい
テストによってはスキップさせたいという場面も出てくると思います。
そういった場合は、スキップさせたい処理に下記の7〜9行目のように if testing.Short() { t.SkipNow() }という記述を追加し、「--short」オプションを指定してテストを実行することで、該当のテストをスキップさせることができます。
....
6
7 if testing.Short() {
8 t.SkipNow()
9 }
10
11 type fields struct {
....
go test ./... -v --short
? sample [no test files]
=== RUN TestUser_GetMessage
--- SKIP: TestUser_GetMessage (0.00s)
PASS
ok sample/handler 0.215s
カバレッジを取得したい
カバレッジを取得する場合は「-cover」オプションをつけて実行します。
「-coverprofile=coverage.out」をつけるとcoverage.outファイルが生成され、下記のようにテスト処理を行った部分を資格てきに確認することができます。「-covermode」オプションは
set: 通過したかしていないかのみ
count: 通過回数をすべてカウント
atomic: 通過回数を並列処理の中でも正確にすべてカウント
を指定できます。
$ go test ./... -v -cover -coverprofile=coverage.out -covermode=set
? sample [no test files]
=== RUN TestUser_GetMessage
=== RUN TestUser_GetMessage/テスト1
=== RUN TestUser_GetMessage/テスト2
--- PASS: TestUser_GetMessage (0.00s)
--- PASS: TestUser_GetMessage/テスト1 (0.00s)
--- PASS: TestUser_GetMessage/テスト2 (0.00s)
PASS
coverage: 100.0% of statements
ok sample/handler 0.190s coverage: 100.0% of statements
$ go tool cover -html=coverage.out
「go tool cover -html=coverage.out」でカバレッジの状態をブラウザで確認できます。今回は単純なコードということで通過した緑の部分だけしかありませんが、テストの通過をしていない箇所は赤色で表示されます。
またcovermodeを「count」もしくは「atomic」を指定していると、緑の部分でマウスをホバーしてしばらくすると通過回数が表示されます。
まとめ
簡単なものですが、Go言語でテストコードを書いてみました。
テストコードを書くことは、コードの保守性や信頼性の確保につながります。きちんと書くことを心がけたいですね。
今回は単純な処理だけでしたが、testingパッケージで幅広いことができますので、ぜひいろいろチャレンジしていただけたらと思います。
最後までお読みいただきありがとうございました。
最後に
現在、レスキューナウでは、災害情報の提供、災害情報を活用した安否確認サービスなどのWebサービスの開発エンジニアを募集しています!
社員・フリーランスに関わらず、参画後に安心してご活躍できることを目指し、応募された方の特性・ご希望にマッチしたチームをご紹介します。
ちょっと話を聞いてみたい、ぜひ応募したい、など、当社にご興味を持っていただけましたら、お気軽にエントリーください!!