単体テストのやり方のイメージが湧かない
これまで業務でperlを使っていたこともあってテストを手動、かつブラウザの画面上から手動で行なっていたため
Goを導入してからローカルでかつ自動でテストを行うパッケージが標準で用意されていると知っても
- 「どのように使えばいいか」
- 「どういう時に使用するのか」
といったイメージが全く湧かない状態でした。
ともあれ、使ってみなければ一生理解できることなどない!ということでtestingパッケージをまずは使ってみようと考えていたところ、paizaの問題の全部または一部をtestingパッケージでテストしてみることを思いつきました。
初めて自動で単体テストをやる人の足掛かりになれば幸いです。
事前のインプット
みんなのGo言語という書籍を読みました。
Go界隈で有名な人が共著で出版していて、業務で必要なGoの基礎知識が書かれた本で、
テストについてもこの書籍の第6章で取り上げられていました。
章単位で取り上げるほどテストが重要項目であるということから、いままで泥臭くやって (は確認漏れが時折出ていて) いた自分のやり方の時代遅れ具合を感じるとともに、自動テストの学習の必要性を強く感じるきっかけにもなりました。
あとはIPAの難関試験で有名なシステムアーキテクト(SA)試験の教本を読んだことで、テストの網羅性などの考え方を汎用的に学習できたと感じました。
ただし内容が難しいため、テスト論理的なものについては私自身もまだ理解不足の点がたくさんあると思います。
paizaの問題を使って一度書いてみる
今回使用するpaizaの問題はこちら。
下記の問題をプログラミングしてみよう!
あなたは、とあるウェブサイトを管理していました。
ある連続したk日間、このウェブサイトでキャンペーンを行ったのですが、いつからいつまでの期間に行ったかを忘れてしまいました。
幸い、ウェブサイトを運営していた全n日分のアクセスログが残っており、1日ごとの訪問者数が分かっています。
とりあえず、連続するk日の中で、1日あたりの平均訪問者数が最も多い期間を、キャンペーンを行った期間の候補だと考えることにしました。
n日分の訪問者数のリストとキャンペーンの日数kが入力されるので、キャンペーンを行った期間の候補数と、候補の中で最も早い開始日を出力してください。
日別訪問者数の最大平均区間という問題です。まずは解答がこちら
スキルチェックではないため、解答を載せても問題はないと思います。
とはいえ今回の目的はGoのユニットテストを行うことでこの問題の解答は趣旨ではないため自力で解きたい方は読み飛ばすことをおすすめします。
package main
import (
"bufio"
"fmt"
"os"
"strconv"
)
var sc = bufio.NewScanner(os.Stdin)
func main() {
// 入力受け取り
sc.Split(bufio.ScanWords)
n := nextInt()
k := nextInt()
// 配列にする
visits := make([]int, n)
for i := 0; i < n; i++ {
visits[i] = nextInt()
}
// 連続するk日の訪問者の合計を算出する
visitSum := []int{}
for i := 0; i < len(visits); i++ {
if i+k > len(visits) {
break
}
sum := 0
for j := i; j < i+k; j++ {
sum += visits[j]
}
visitSum = append(visitSum, sum)
}
// 出力値を取得し、出力
candidate, first := candidate(visitSum)
fmt.Println(candidate, first)
}
func candidate(ints []int) (int, int) {
count := 0
first := 0
// 合計値が最大のものを取得
m := max(ints)
// 最大値と同値のものを見つけたら最初に最大値を要素に持っていた番地を記録しつつ
// 最大値の入った配列の要素の個数を計上する
for i, v := range ints {
if v == m {
if count == 0 {
first = i + 1
}
count++
}
}
return count, first
}
func max(ints []int) int {
max := ints[0]
for i := 1; i < len(ints); i++ {
if max < ints[i] {
max = ints[i]
}
}
return max
}
func nextInt() int {
sc.Scan()
i, err := strconv.Atoi(sc.Text())
if err != nil {
panic("cannot convert string to int")
}
return i
}
全体で単体テストを行ってもよいのですが、今回はこの回答の中で
連続するk日の訪問者の合計をまとめた配列から最大値を取得する関数について単体テストをしたいと思います。
その関数がこちら。
func max(ints []int) int {
max := ints[0]
for i := 1; i < len(ints); i++ {
if max < ints[i] {
max = ints[i]
}
}
return max
}
これに対してのテストコードがこちら
func Test_max(t *testing.T) {
type args struct {
ints []int
}
tests := []struct {
name string
args args
want int
}{
// TODO: Add test cases.
{name: "先頭が最大値", args: args{ints: []int{6, 5, 4, 5, 3, 2, 1, 1}}, want: 6},
{name: "途中が最大値", args: args{ints: []int{1, 2, 5, 4, 6, 4, 3}}, want: 6},
{name: "末尾が最大値", args: args{ints: []int{1, 2, 3, 4, 5, 6}}, want: 6},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := max(tt.args.ints); got != tt.want {
t.Errorf("max() = %v, want %v", got, tt.want)
}
})
}
}
テストコードの構造としましては
- 引数およびテストデータをstruct化する
- テストデータは名前(そのケースで失敗したか見やすくするため), 入力値(引数など), 出力値で構成される
- 各テストケースのstructをテストしたい関数に流し込んで、戻り値が想定する出力値と等しいかどうか検証する
という言葉にしてしまうとシンプルなものです。
VSCodeのGoの拡張機能を入れれば、unitテスト用のgoファイルを自動生成できるのでそれを使うことをおすすめします。
次にテストケースですが、配列を探索して最大値を返す場合、以下の場合についてテストできれば網羅性を保てます。
- リストの先頭に最大値がある
- リストの途中に最大値がある
- リストの末尾に最大値がある
よって、今回のテストケースにおける入力値はこの3つの条件に対応する配列となります。
実行はVSCodeにGoの拡張機能とローカルにGoのランタイム環境を用意している方はエディタからそのまま行えます。ない場合はコマンドで実行します。
go test max_test.go
ok paiza/max_range 0.133s
もしくは
go test ./...
ok paiza/max_range 0.131s
まとめ
- テストケースさえ落ち着いて考えることができればGoの単体テストはとても簡単
- 複雑に考えすぎずにまずは間違えてもいいのでテストケースを登録して実行してみる
- DBから情報を取得したりhttpリクエストが間に入るときは、mockというものを使う。これは、要は受け取って来た体でテストを進めるということである
- いきなりmockが必要な関数についてテストするより、今回のような手書きのテストケースで簡単に自動テストができる内容の関数で一度成功体験を作った方が理解が深まる(と個人的には考える)