2022/02/13

[golang] testing

コードを書いたら極力テストは行うべきだ(戒め)。
というわけで、golang のテストを調べる。

過去の私もいくらかテストについては調べていたようだ。

hiro99ma blog: [golang]本体無しでテストをしたい (1)
https://blog.hirokuma.work/2020/03/golang-1.html

しかし、もう記憶にない。

最近は fuzzing というのもあるようだが、golang v1.18 以降らしいので扱わない。


まず、コード以外のルールから。

  • ファイル名は _test.go で終わるようにする
    • そうすると go build の対象にはならず go test の対象になる
  • "testing" を import する
  • 関数名は "Test" で始まるようにする
    • そうするとテスト関数という扱いになる
  • 関数名で "Test" の次は大文字で始める

関数名はともかく、ファイル名は気をつけねば。テスト用じゃないのに _test.go と付けてしまうことがあり得なくもないからだ。golang って大文字小文字ルールもそうだけど、名前で決められていることがしばしばあるからあせるね。

あれ、golang の関数名は UTF-8 を使えるのだろうか?
使える場合、日本語なんかは大文字扱いされるのだろうか?

 

main.go

01: package main
02: 
03: import (
04:     "fmt"
05: )
06: func よしお() string {
07:     return "よしお"
08: }
09: 
10: func main() {
11:     fmt.Printf("%s\n", よしお())
12: }

main_test.go

01: package main
02: 
03: import "testing"
04: 
05: func Testよしお(t *testing.T) {
06:     yoshio := よしお()
07:     if yoshio != "よしお" {
08:         t.Error(`not yoshio`)
09:     }
10: }

$ go test -v
=== RUN   Testよしお
--- PASS: Testよしお (0.00s)
PASS
ok      github.com/hirokuma/go-test1    0.012s

よいらしい。
t.Error() の中がバッククォートになっているが、これは本でそう書いてあったからだ。別にバッククォートである必要はない。
JavaScript だとバッククォートなら ${xxx} みたいにして変数を展開できるが、golang だと bash のヒアドキュメントみたいにそのまま展開されるというだけのようだ。 raw string literal というらしい。

 

ちなみに、Test の次を小文字にするとテスト関数と見なされず実行されなかった。
一時的に実行したくない場合なんかによいかもしれぬ。

 

あと、本に書いてあることで興味があるのはカバレッジだろうか。
ただ載っているとおり -run=Coverage だとテストが実行されなかった。

$ go test -v -run=Coverage -coverprofile=c.out
testing: warning: no tests to run
PASS
coverage: 0.0% of statements
ok      github.com/hirokuma/go-test1    0.003s

-cover だと実行された。

$ go test -v -cover -coverprofile=c.out
=== RUN   Testよしお
--- PASS: Testよしお (0.00s)
PASS
coverage: 50.0% of statements
ok      github.com/hirokuma/go-test1    0.003s

結果は、載っているとおりに実行するとブラウザが立ち上がった。

go tool cover -html=c.out

image

まあ、main関数は仕方ない。

と思ったが、main関数もテストできるのか。

Test the main function in Go | GO.into Development
https://mj-go.in/golang/test-the-main-function-in-go

main関数は特別なものだと思っていたが、go test なら普通に呼び出せるのか。

func TestMain(t *testing.T) {
    main()
}

普通に呼べた。

image

 

go test できれいにテストを回したいなら、実装の方も多少はテストを意識して実装する必要があるだろう。
めんどくさいという気もするが、その方が実装も小さい単位になってメンテナンスしやすくなることも多い。そこまで悪くない話だと思っている。
main関数も、中に全部書いてしまうのではなく本体は別関数にして戻り値をテストしやすくする、みたいな。

私はそこら辺が苦手な気がする。
なんというか、見える範囲に揃っていないとよくわからなくなるというか。
コース料理よりも丼にいろいろ載っている方がうれしいというか。
誰に言い訳してるんだ、私は。


私が作ったことがある golang のテストは、RC-S370/S のエンコードくらいだった。

go_pasori370/msg_test.go at master · hirokuma/go_pasori370
https://github.com/hirokuma/go_pasori370/blob/master/dev/msg_test.go

よく見ると t.Fatail を使っているな。
t.Error は継続し、t.Fatal は止めるようだ。
私は途中で止めてしまいたいのだが、一通り全部通してみたいと考える方が普通なのかもしれないがどうなんだろう?

なるほど、最初のエラーで止めるオプションがあるのか。

unit testing - Stop on first test failure with `go test` - Stack Overflow
https://stackoverflow.com/questions/32046192/stop-on-first-test-failure-with-go-test

Testよしお() で失敗するようにして試してみよう。

まずは普通に実行すると、Testよしお() も TestMain() も実行されている。

$ go test -v
=== RUN   Testよしお
    main_test.go:8: not yoshio
--- FAIL: Testよしお (0.00s)
=== RUN   TestMain
よしお
--- PASS: TestMain (0.00s)
FAIL
exit status 1
FAIL    github.com/hirokuma/go-test1    0.014s

これを -failfast オプションを付けると、 TestMain() は実行されていない。

$ go test -v -failfast
=== RUN   Testよしお
    main_test.go:8: not yoshio
--- FAIL: Testよしお (0.00s)
FAIL
exit status 1
FAIL    github.com/hirokuma/go-test1    0.003s

開発後半になって、テスト実行にものすごく時間がかかるようになった場合なんかによいのかもしれない。


直接 go test とは関係ないのだが、どこでエラーが起きたかの根源を知りたいことは多い。
error を返すようにしたとして、"InvalidParam: -20 < 0" とか返されたとしても困るのだ。
コマンドを実行してエラー、とかなら因果関係がわかりやすいのだが、サーバー的なプログラムでログの中にエラーが残っていたとしても追跡できなくては意味が無い。

がんばって fmt.Errorf() なんかで書いていくのだけど、まあ面倒だ。
エラーにした理由はもちろん、自分のパッケージ名や関数名を書かないと誰だか分からないからだ。
そしてそれをログにも出すのか return するだけなのかも結構悩む。その関数が A からも B からも呼ばれるようであれば自分の情報を返すだけではなく、呼び出し元の方でも何かやってもらわないと追えないからだ。ログに出せば間違いはないのかもしれないが、ログがたくさん出るというのもあまりうれしくない。

 

今までどうしてきたかというと、組み込み開発だとそもそもログを出せる状況にないことがほとんどだったし、文字で出せないからビットのパターンで表したりとかしてたしね。エラーになっても仕方ないのでそもそもリセット掛けて再起動するし。
そんなわけで、私のエラー処理は初心者レベルだと認識している。
精進がいりますな。