Featured image of post Goで無限ループのテストの書き方がわかんなかった

Goで無限ループのテストの書き方がわかんなかった

鬼滅のテスト 無限ループ編

わかんなかったので自分なりにやってみたメモ
サブタイトルはおふざけ


まとめ

ループ1回分の処理を抜き出して, それをテストするのが良さそう.
ループ1回分のモックが作れるなら, 無限ループの部分を呼び出して任意の回数でループを止めることもできる.

(無限ループを含むコード例)

(テストコード例)

環境

  • macOS Mojave 10.14
  • go version go1.14.6 darwin/amd64

やりかた

Goでこんな感じの無限ループ処理があるときに,
どうやってテストを書いたらいいかわかんなかった.

# 処理としてはhogehoge.txtの内容を1秒ごとに読み込んで出力するだけ
$ echo fugafuga > hogehoge.txt
$ go run main.go
fugafuga

fugafuga

fugafuga

fugafuga

fugafuga

^Csignal: interrupt
# 無限ループなので強制終了する

テキトーに調べたけど1, 提案されているのは

  • ループの回数を指定できるような仕組みを仕込んでおいてテストのときだけ回数を有限にする
  • ループ1回分の処理だけをテストするようにする

のどっちかという感じだった.

自分の場合はDI(Dependency Injection)以外でテストのためだけに変な引数を追加したりしたくないので, ループ1回分だけを取り出してテストするほうが良いかな, と思った.

まずはmain.goを修正して, ループ1回分の処理を別のメソッド(Loop.Run)に分ける.
(実行される処理の内容は変わらない)

次にgomock2でループ1回分のinterface(Loop)のモックを作る.

# go.modの作成($GOPATH配下で作業している場合は不要)
$ go mod init github.com/uzimihsr/infinite-loop-test
go: creating new go.mod: module github.com/uzimihsr/infinite-loop-test

# mockの生成
$ go get github.com/golang/mock/mockgen@v1.4.4
$ mockgen -source=./main.go -destination=mock_main.go -package=main Loop
mock_main.go(生成されたモック)

(後で気づいたけど, main関数のテストはしないのでちゃんとパッケージを分ければよかった…)

ループ1回分(Loop.Run)のテストと無限ループ(InfiniteLoop.Run)のテストを作成する.

ループ1回分のテストでは実際に1回分の処理だけをテストしている.
(今回はloop.Run()内部で標準パッケージのioutil.ReadFile()を直接呼び出してしまっているけど, 可能であればここも依存している関数やメソッドのモックを使ったほうが良い気がする)

無限ループのテストではmockgenで生成したループ1回分のモックを利用して,
任意の回数を超えた時点でエラーを発生させてループが止まるようにしている.
こうすることで, 結果的には無限ループの回数を指定してテストしているのと同じことができた.

実際にテストを実行してみる.

# 最終的な作業ディレクトリの状態
$ tree .
.
├── go.mod
├── go.sum
├── hogehoge.txt
├── main.go
├── main_test.go
├── mock_main.go
└── test-hogehoge.txt

# テスト実行
$ echo test-fugafuga > test-hogehoge.txt
$ go test -v -cover ./...
=== RUN   TestLoop
=== RUN   TestLoop/正常にファイルが開けるケースのテスト
test-fugafuga

=== RUN   TestLoop/ファイルが開けず異常終了するケースのテスト
--- PASS: TestLoop (0.00s)
    --- PASS: TestLoop/正常にファイルが開けるケースのテスト (0.00s)
    --- PASS: TestLoop/ファイルが開けず異常終了するケースのテスト (0.00s)
=== RUN   TestInfiniteLoop
=== RUN   TestInfiniteLoop/ループが1回正常に呼び出されることのテスト
=== RUN   TestInfiniteLoop/ループが任意の回数(例えば10回)正常に呼び出されることのテスト
--- PASS: TestInfiniteLoop (11.03s)
    --- PASS: TestInfiniteLoop/ループが1回正常に呼び出されることのテスト (1.01s)
    --- PASS: TestInfiniteLoop/ループが任意の回数(例えば10回)正常に呼び出されることのテスト (10.02s)
PASS
coverage: 63.4% of statements
ok  	github.com/uzimihsr/infinite-loop-test	11.034s	coverage: 63.4% of statements

mock_main.gomainパッケージに入ってたりmain関数のテストができていなかったりで,
カバレッジは100%になっていないけど一応やりたかった内容は実行できた.

テストの目的はカバレッジを100%にすることではないので, これでいいはず…

おわり

無限ループがあるコードを書いた経験があまりなかったので, けっこう詰まってしまった.

自分なりにやってみたけど, 他にもっといい方法があるんだろうか…
もっと厳密にやりたいならosパッケージ3を呼び出してプロセスを扱ったりしてもいいんだろうけど, そこまでする必要があるかは謎🤔
(個人的には"無限に繰り返されること"の確認にそこまでの価値があると思えなかった)

おまけ

無限にだらだらするのが得意なねこ