Edited at
Tech DoDay 22

Go言語でテストコードを書いてみた!


はじめに

こんにちは!今回Tech Do Advent Calendar 22日目を担当する、新卒2年目のやまさわです。

Go言語歴1ヶ月弱の超初心者が、Go言語でテストコードを書いてみたお話をしたいと思います。

テストコード自体ほとんど書いたことがないので、間違っていたら優しく教えてくれると嬉しいです。


Go言語でテストするには?

テスト全般を支援するtestingパッケージを利用します。

このパッケージでは、ユニットテストやベンチマークテストがサポートされています。

今回はユニットテストについて説明します。


● テストコードのルール

テストコードを実装する際、3つのルールがあります。


  1. パッケージ :testingパッケージをインポートする

  2. ファイル名 :xxx_test.go

  3. テスト関数名:func TestXxx (t *testing.T)

このルールに則ることで、コードをビルドする時にテストコードが除外され、テスト実行コマンドでテストコードのみがビルドされるようになります。


● 構成

テストコードとテスト対象のコードを同じディレクトリに配置するとテストしやすいです。

tech_do

┣━ main.go
┗━ hello
   ┣━ hello.go
   ┗━ hello_test.go


● 早速テストコードを書いてみる!

早速テストコード書いてみます。

その前に、まずはテスト対象のコードです。

▼サンプルコード


main.go

package main

import (
"fmt"
"./hello"
)

func main() {
s := hello.GetHello("山澤さん")
fmt.Println(s)
}



hello.go

package hello

func GetHello(s string) string {
return "こんにちは、" + s + "!"
}


引数に名前を入れると名前付きで挨拶してくれる、とっても簡単なプログラムです。


▼実行結果

このサンプルコードの実行結果はこちらです。

こんにちは、山澤さん!


▼テストコード

サンプルコードに対するテストコードです。


hello_test.go

package hello

import (
"testing"
)

func TestHello(t *testing.T) {
result := GetHello("山澤さん")
expext := "こんにちは、山澤さん!!"
if result != expext {
t.Error("\n実際: ", result, "\n理想: ", expext)
}

t.Log("TestHello終了")
}


ルールに則り、testingパッケージをインポートし、命名規則通りのファイル名とテスト関数名になっています。

このテストコードでは、mainの実行で出力される文字列が、期待通りのものかをチェックしているものです。

実際の値と理想の値が異なるとき、Error関数でエラーが発生したこととログを記録します。

このテストコードを実行する前に、もう少しテストコードの書き方を見ていきたいと思います。


● テスト状態を管理する関数

先程のテストコードでは、Error()でテストの失敗を検知し、ログを出力していました。

これ以外にもたくさんの種類があり、使い分けるとテストが捗ると思います。

▼Log, Logf

引数の文字列をログに記録する関数です。

これらの関数だけでは、テストが成功したのか失敗したのか分かりません。

t.Log(引数1, 引数2, ...)

t.Lof(フォーマット, 引数1, 引数2, ...)


▼Fail, FailNow

エラーがあったことを記録します。

Failはそれ以降の処理を継続し、FailNoeはそれ以降の処理が継続されずテストが終了します。

t.Fail()

t.FailNow()


▼Error, Errorf

エラーがあったことと引数の文字列をエラーログに記録します。

それ以降の処理は継続します。

→Log()とFail()を呼び出すことと同じ!!

t.Error("\n実際: ", result, "\n理想: ", expext)

t.Errorf("\n実際: %v\n理想: %v", result, expext)


▼Fatal, Fatalf

エラーがあったことと引数の文字列をエラーログに記録します。

それ以降の処理は継続されずテストが終了します。

→Log()とFailNow()を呼び出すことと同じ!

t.Fatal("\n実際: ", result, "\n理想: ", expext)

t.Fatalf("\n実際: %v\n理想: %v", result, expext)


▼Skip

そのテストをスキップし、以降の処理へと進みます。

t.Skip()


▼Failed

テストの成功・失敗をbooleanで返却します。

t.Failed()



テスト実行コマンド

テストを実行するコマンドは下記の通りです。


  • go test      :テスト実行

  • go test -v    :テスト実行(詳細な実行結果出力)

  • go test -cover  :テスト実行+コードカバレッジ

  • go test -cover -v :テスト実行+コードカバレッジ(詳細な実行結果出力)

go test、たったこれだけでテストを実行することができますが、オプションを付けることでよりテストが充実すると思います。

特にコードカバレッジのオプションを追加することで、テストで通過したソースコードの割合がパーセンテージで表示されるようになるため、到達不能コードの発見やボトルネック部分の判明が期待できます。


● テスト成功時

▼go test

$ go test

PASS
ok _/C_/workspace/tech_do/hello 0.029s

今回テストが成功しているので、「PASS」と表示されています。


▼go test -v

$ go test -v

=== RUN TestHello
--- PASS: TestHello (0.00s)
hello_test.go:15:
TestHello終了
PASS
ok _/C_/workspace/tech_do/hello 0.061s

vオプションによって詳細な実行結果が表示されるようになり、Log関数のログ内容や行数が表示されます。


▼go test -cover

$go test -cover

PASS
coverage: 100.0% of statements
ok _/C_/workspace/tech_do/hello 0.036s

サンプルコードを通過した割合が出力されています。

今回は100%の到達率です!


▼go test -cover -v

$go test -cover -v

=== RUN TestHello
--- PASS: TestHello (0.00s)
hello_test.go:15:
TestHello終了
PASS
coverage: 100.0% of statements
ok _/C_/workspace/tech_do/hello 0.031s

vオプションで同じく実行結果が詳細に出力されます。


● テスト失敗時

では、テスト失敗時はどのように実行結果が出力されるでしょうか?

先程のテストコードの、期待する文字列を「こんにちは、山澤さん!」から「こんにちは、山澤さん!!」に変更してみてテストを実行してみましょう。

▼go test

$ go test

--- FAIL: TestHello (0.00s)
hello_test.go:12:
実際: こんにちは、山澤さん!
理想: こんにちは、山澤さん!!
hello_test.go:15:
TestHello終了
FAIL
exit status 1
FAIL _/C_/workspace/tech_do/hello 0.042s

テストが失敗しているので「Fail」と表示され、Error関数どおりに実際の値と理想の値がエラーログも出力されます。

また、テスト成功時はLog関数のログは出力されていませんでしたが、テスト失敗時はvオプションに関係なくLog関数も出力されるのが特徴です。


▼go test -v

念のため、vオプションありの場合も。

双方に大きな違いはないことがお分かりいただけるかと思います。

$ go test -v

=== RUN TestHello
--- FAIL: TestHello (0.00s)
hello_test.go:12:
実際: こんにちは、山澤さん!
理想: こんにちは、山澤さん!!
hello_test.go:15:
TestHello終了
FAIL
exit status 1
FAIL _/C_/workspace/tech_do/hello 0.058s




ここまでhelloパッケージ内でテストを実行してきましたが、範囲を絞ってテストを実行することも可能です。

テスト効率を高めるためにも、柔軟にテストを実行すると良いと思います。



● パッケージ単位の実行

指定したパッケージにある全てのxxx_test.goのテストを実行します。

※ルートディレクトリに移動すること

go test ./hello


● テスト関数単位の実行

指定したテスト関数のみを実行します。

※テストコードのあるディレクトリに移動すること

go test -run TestHello


● プログラム単位の実行

指定したプログラムのみを実行します。

パッケージ内に複数のテストコードがあって、そのうちの一つだけを実行したいときに有用です。

go test hello_test.go hello.go

go test hello/hello_test.go hello/hello.go


少し応用のテスト

ここまでは、Go言語でユニットテストをするときに最低限押さえておきたいことについてでした。

ここからはもう少し応用のお話をしたいと思います。


● レシーバ付きの関数

テスト関数名にレシーバの型を入れる必要があります。


  • 通常時  :TestXxx

  • レシーバ時:Test{レシーバ型}_Xxx


● サブテスト

サブテストは、テスト関数の中でテストケースが複数ある時に利用できます。

そのテストケースを一意にするために第一引数で名前を指定して、テストケースで行う処理をfuncに記載します。

レシーバ

t.Run(サブテストの名前, 関数)


● テストコード:レシーバ、サブテスト

レシーバとサブテストがある場合、サンプルコードやテストコードの書き方を見てみましょう。

▼サンプルコード


sum.go

package calc

type Sum struct{}

func (a *Sum) SumMulti(n ...int) int {
var sum int
for _, m := range n {
sum += m
}

return sum
}


このサンプルコードは足し算をする処理で、指定された引数分足していきます。

レシーバはSumです。


▼テストコード


sum_test.go

package calc

import "testing"

func TestSum_SumMulti(t *testing.T) {

t.Run("Len=1", func(t *testing.T) {
t.Log("Len=1")
if new(Sum).SumMulti(1) != 1 {
t.Fail()
}
})

t.Run("Len=2", func(t *testing.T) {
t.Log("Len=2")
if new(Sum).SumMulti(1, 2) != 3 {
t.Fail()
}
})

t.Run("Len=3", func(t *testing.T) {
t.Log("Len=3")
if new(Sum).SumMulti(1, 2, 3) != 6 {
t.Fail()
}
})
}


命名規則通り、テスト関数名にレシーバのSumが入っています。

今回サブテストはそれぞれ引数が1つ、2つ、3つのケースで分かれています。


▼実行結果

今のテストを実行するとこのように、サブテストの名前ごとに実行結果が出力されます。

$go test -v

=== RUN TestSum_SumMulti
=== RUN TestSum_SumMulti/Len=1
=== RUN TestSum_SumMulti/Len=2
=== RUN TestSum_SumMulti/Len=3
--- PASS: TestSum_SumMulti (0.00s)
--- PASS: TestSum_SumMulti/Len=1 (0.00s)
sum_test.go:8: Len=1
--- PASS: TestSum_SumMulti/Len=2 (0.00s)
sum_test.go:15: Len=2
--- PASS: TestSum_SumMulti/Len=3 (0.00s)
sum_test.go:22: Len=3
PASS
ok _/C_/workspace/tech_do/calc 0.041s


● サブテスト単位の実行

パッケージ単位やテスト関数単位のように、サブテスト単位でもテストを実行することが可能です。

指定する際は、「関数名/サブテスト名」となります。

$go test -v -run SumMulti/Len=1

=== RUN TestSum_SumMulti
=== RUN TestSum_SumMulti/Len=1
--- PASS: TestSum_SumMulti (0.00s)
--- PASS: TestSum_SumMulti/Len=1 (0.00s)
sum_test.go:8: Len=1
PASS
ok _/C_/workspace/tech_do/calc 0.042s


● 前処理・後処理

テストをする際は、テスト開始前・終了後に何か処理を入れたいこともあると思います。

そういった際にはTestMain関数を利用しましょう。

TestMainの中にm.Run()を書くことで、そのプログラムに記述されているテスト関数が順次実行されます。

例えばm.Run()を2回書いたら、ファイルの内のテスト関数がそれぞれ2回実行されます。

m.Run()を書かないと、テスト関数は実行されないので注意が必要です。


● 並列処理

サブテストのRun関数の中に、Parallel関数を記載すると並列処理になります。

並列処理する複数のサブテストをt.Run(name, func)でラップすることで、全てのサブテストが終了して初めてその該当箇所のテストが終了した扱いとなります。


● テストコード:前処理・後処理、並列処理

▼テストコード


sum_test.go

package calc

import "testing"

func TestMain(m *testing.M) {
println("[test start]")
m.Run()
println("[test finish]")
}

func TestSum_SumMulti(t *testing.T) {
t.Run("group", func(t *testing.T) {
t.Run("Len=1", func(t *testing.T) {
// サブテストを並列実行する
t.Parallel()
// sleepで終了タイミングをずらし、並列実行を確認する
time.Sleep(time.Second * 2)
/*中略*/
})

t.Run("Len=2", func(t *testing.T) {
t.Parallel()
/*中略*/
})
})
}


TestMain関数には、テスト前処理でテスト開始の旨を、テスト後処理でテスト終了の旨を出力するように記述しています。

前処理・後処理に挟まれているm.Run()によって、その後のTestSum_SumMulti()が実行されているのです。

t,Run()内にt.Parallel()を追記して並列処理に、2つのテストケースはt.Run("group", func(t *testing.T) {}でラップされていることもお分かりいただけるかと思います。

今回は、「Len=1」と「Len=2」が並列処理になっていることを確認しやすくするために、1つ目のサブテストにSleepをいれて、テスト終了のタイミングをコントロールしいます。


▼実行結果

$go test -v

[test start]
=== RUN TestSum_SumMulti
=== RUN TestSum_SumMulti/group
=== RUN TestSum_SumMulti/group/Len=1
=== PAUSE TestSum_SumMulti/group/Len=1
=== RUN TestSum_SumMulti/group/Len=2
=== PAUSE TestSum_SumMulti/group/Len=2
=== CONT TestSum_SumMulti/group/Len=1
=== CONT TestSum_SumMulti/group/Len=2
--- PASS: TestSum_SumMulti (2.01s)
--- PASS: TestSum_SumMulti/group (0.00s)
--- PASS: TestSum_SumMulti/group/Len=2 (0.00s)
sum_test.go:25: Len=2
--- PASS: TestSum_SumMulti/group/Len=1 (2.00s)
sum_test.go:17: Len=1
PASS
[test finish]
ok _/C_/workspace/tech_do/calc 2.069s

テストを実行すると、初めに「Len=1」が実行されていますが、Sleepが入っているので「Len=2」が先にテスト完了しています!

この二つのサブテストが並列処理であったことがお分かりいただけるかと思います。


おまけ:ブラウザテスト

おまけで、Go言語でブラウザテストをするにはどうすればいいのかについても、簡単にまとめました。

事前準備として、「Selenium WebDriver」と「agouti」をインストールします。


● Selenium WebDriver

ブラウザでWebアプリケーションをテストするツールです。

公式サイトからも、コマンドからもインストールすることができます。

インストール後、環境変数でパスを通すことを忘れないようしてください!

下記、上のコマンドがWindows用、下のコマンドがMac用です!

$ cinst selenium-chrome-driver

$ brew install chromedriver


● agouti

WebDriverクライアントで、Go言語のテストフレームワークです。

Goコマンドからインストール可能です。

$ go get github.com/sclevine/agouti


● テストコード

ブラウザテストをする際の基本の書き方はこちらです。

1. agoutiをインポートする

2. ドライバの設定

3. ページ表示

4. テストしたい処理

5. (Sleep処理)

5のSleep処理は、テスト完了後もブラウザを表示するためのものなので、秒数や書くか書かないかは自由です。


▼ブラウザテストのコード


main.go

package main

import (
"github.com/sclevine/agouti"
"log"
"time"
)

func main() {
// ドライバ:Chrome
driver := agouti.ChromeDriver(agouti.Browser("chrome"))
if err := driver.Start(); err != nil {
log.Fatalf("ドライバエラー:%v", err)
}
defer driver.Stop()

// ページ表示
page, err := driver.NewPage()
if err != nil {
log.Fatalf("ページエラー:%v", err)
}

// ここからテストしたい処理を書く
// ログインページに遷移
if err := page.Navigate("https://mechacomi.jp/sd/page/login/0000000a/?force=1"); err != nil {
log.Fatalf("遷移エラー:%v", err)
}

// ID要素を取得し、値を設定
page.FindByClass("navi2").Click()
id := page.FindByName("account")
password := page.FindByName("pass")
id.Fill("【ID】")
password.Fill("【PASS】")

// ログインac
if err := page.FirstByClass("layout-login").Submit(); err != nil {
log.Fatalf("ログインエラー:%v", err)
}

// 処理完了後、10秒間ブラウザを表示しておく
time.Sleep(10 * time.Second)
}



▼テストしたい処理

今回私は「テストしたい処理」に、弊社の電子書籍のサイト「コミなび」のアカウントログイン処理を実装しました。

処理の流れは、コードからもお分かりいただけますが、下記の通りです!


・ログインページへの遷移

・IDとPassを入力するフォームの要素の取得

・ID・Passの設定

・ログインボタン押下


実際にこのプログラムを実行すると、ブラウザの表示からログイン、そしてブラウザを閉じるところまで、全て自動で行ってくれました!


終わりに

最後に、Go言語の魅力についてまとめです。


  • 普段テストコードをあまり書いたことがなかったのですが、Go言語はテストコードもシンプルで直観的に書くことができると思います。

  • 標準ライブラリはもちろん、testingというテスト用ライブラリがあり、様々なテストのサポートが充実しているのですごくオススメです。

  • WebDriver + agoutiを利用することで、ブラウザテストも非常に簡単にできました。

  • ブラウザテストでは、テスト目的以外にもブラウザ上で定期的に行う手作業を自動化することも可能です。



いかがでしたか?

Go言語でのテストをあまり知らない人からすると、「こんなに簡単にテストできるんだ!」と思われたのではないでしょうか。

(私が今回調べた時にすごく思ったので。。)

ただ、Go言語が得意な方にとってはどれもこれも周知の事実…かもしれないですね…(笑)

今回の発表で、Go言語でのテストが簡単だということが皆さん伝わっていれば幸いです!

普段業務で使用されていない方も、是非Go言語でテストコードを書いてみたり、ブラウザテストをしてみたり、自動化プログラムを作ってみたりしてください。

最後までお読みいただきありがとうございました♪