🧪

gotests を使ってテーブルドリブンテストを簡単に作成する

2021/05/29に公開

Golang の推奨されるテスト方法の1つに「テーブルドリブンテスト」というものが存在します。

詳細については公式 wiki にもあるのでそちらをご参照ください。
https://github.com/golang/go/wiki/TableDrivenTests

var flagtests = []struct {
	in  string
	out string
}{
	{"%a", "[%a]"},
	{"%-a", "[%-a]"},
	{"%+a", "[%+a]"},
	{"%#a", "[%#a]"},
	{"% a", "[% a]"},
	{"%0a", "[%0a]"},
	{"%1.2a", "[%1.2a]"},
	{"%-1.2a", "[%-1.2a]"},
	{"%+1.2a", "[%+1.2a]"},
	{"%-+1.2a", "[%+-1.2a]"},
	{"%-+1.2abc", "[%+-1.2a]bc"},
	{"%-1.2abc", "[%-1.2a]bc"},
}
func TestFlagParser(t *testing.T) {
	var flagprinter flagPrinter
	for _, tt := range flagtests {
		t.Run(tt.in, func(t *testing.T) {
			s := Sprintf(tt.in, &flagprinter)
			if s != tt.out {
				t.Errorf("got %q, want %q", s, tt.out)
			}
		})
	}
}

このようにテスト対象となるデータの構造体をスライスにしておいて、それをループ処理にかけることによってテストを実行する方法です。
正常値、異常値、閾値など色んなパターンのテストを漏れがないようにかつ簡潔に書ける方法としておすすめです。

しかし、このテストを毎回自分の手で作成するのはちょっと面倒です。
テストを書くハードルはなるべく下げて、テストをしっかり書くチームにしたいです。

そこで使いたいのが、今回紹介する cli ツールの gotests です。
https://github.com/cweill/gotests
gotests は cli ツールなので、感覚としては go importsgo vet のように開発を助けてくれるものになります。
アプリケーションに変な依存関係を持たせるものでは無いのでご安心ください🙌

この記事は、

go version go1.16.3 darwin/arm64

で動作しています。

やる😎

プログラムの題材は何でも良いのですが、とりあえず飯でも尋ねておきましょう。
「朝・昼・晩」どれかを含む文字列を引数に渡すと該当するメニューを返してくれる Meal 関数を作ります。

package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(Meal("朝ご飯は何?"))
}

func Meal(s string) string {
	switch {
	case strings.Contains(s, "朝"):
		return "トースト"
	case strings.Contains(s, "昼"):
		return "そば"
	case strings.Contains(s, "晩"):
		return "ハンバーグ"
	}

	return "どれにもマッチしませんでした。"
}

gotests を入れます。

$ go get -u github.com/cweill/gotests/...

雛形を作成します。

$ gotests -only Meal -w .
package main

import "testing"

func TestMeal(t *testing.T) {
	type args struct {
		s string
	}
	tests := []struct {
		name string
		args args
		want string
	}{
		// TODO: Add test cases.
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := Meal(tt.args.s); got != tt.want {
				t.Errorf("Meal() = %v, want %v", got, tt.want)
			}
		})
	}
}

-only オプションを付けて Meal 関数のテストだけを作成するようにしました。オプションは複数あるので公式ドキュメントをご参照ください👨‍💻
デフォルトでは標準出力に吐き出されますが、多くの場合はテストファイルに書き込んで欲しいと思うので、 -w オプションも付けています。

中身を見ると、 Meal 関数の引数を認識して args 型を定義してくれています。
テストの名前として付けるための name と期待値 want も string 型で定義されています。
非常に分かりやすいですね!!

テストケースを追加すると以下のようになりました。

package main

import "testing"

func TestMeal(t *testing.T) {
	type args struct {
		s string
	}
	tests := []struct {
		name string
		args args
		want string
	}{
		{"トーストが返ること", args{s: "朝"}, "トースト"},
		{"そばが返ること", args{s: "昼"}, "そば"},
		{"ハンバーグが返ること", args{s: "晩"}, "ハンバーグ"},
		{"マッチしないこと", args{s: "hogehoge"}, "どれにもマッチしませんでした。"},
		{"トーストが返ること", args{s: "朝ご飯は何?"}, "トースト"},
		{"そばが返ること", args{s: "昼ご飯は何?"}, "そば"},
		{"ハンバーグが返ること", args{s: "晩ご飯は何?"}, "ハンバーグ"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := Meal(tt.args.s); got != tt.want {
				t.Errorf("Meal() = %v, want %v", got, tt.want)
			}
		})
	}
}

IN と OUTが明確で後からの追記も簡単なテストができました。
テストケースを追加したい場合には1行追加するだけでOKです。

gotests 便利🧐


テーブルドリブンテストの雛形を簡単に作成することができました。
gotests は積極的に使っていきたいツールになりそうです😊

Discussion