前書き
- 私のPC & OS
- 今回のソースコードはこちら
https://github.com/wataboru/golang-test-driven-dev
前準備
プロジェクト作る
- Golandにて、
File -> New -> Project...
ディレクトリ作成
今回はサンプルとして型変換処理を作成します。
以下の通りディレクトリを作成
- goalng-test-driven-dev(root)
∟ pkg
∟ convert
処理の定義を行う
後ほどテストを記述する際に、Golandが補完してくれて便利なので、先に定義してしまいましょう。
-
/pkg/convert
にGoファイルを作成。 - 今回はstring型へ型変換処理を行う関数を作成
- 引数の型は自由とし、戻り値の型はstringとする。
- とりあえずコンパイルが通る様に無理やりstringを返却する。
package convert
func ToString(v interface{}) (string) {
return ""
}
テスト駆動で開発してみる
テスト書く
Golandはテストファイルの自動生成が便利です。
以下の通り利用しましょう
- テストしたいfuncやファイル内にフォーカスした状態で、
Cmd + Shift + T
-
Test for function
やTest for file
等、自動生成したい範囲を選択 - 以下の通り自動生成されたら、
// TODO:
にケースを追記していく - 今回はとりあえず、期待値としてstring,bool,intを受け取る様にした。
package convert
import "testing"
func TestToString(t *testing.T) {
type args struct {
v interface{}
}
tests := []struct {
name string
args args
want string
}{
{name: "String", args: args{v: "hoge"}, want: "hoge"},
{name: "Int", args: args{v: 1},want: "1"},
{name: "Bool", args: args{v: true}, want: "true"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ToString(tt.args.v); got != tt.want {
t.Errorf("ToString() = %v, want %v", got, tt.want)
}
})
}
}
テストを実行する
テストを書いたらすぐに実行しましょう。
もちろん、まだ処理を書いていないのでエラーが発生するはずですね。
処理を実装する
それでは、テストが通る様に実装をしていきましょう
テストの期待値を満たす様に、型別にstringへ変換してreturnする様にします。
package convert
import (
"strconv"
)
func ToString(v interface{}) string {
switch vt := v.(type) {
case string:
return vt
case int:
return strconv.Itoa(vt)
case bool:
return strconv.FormatBool(vt)
default:
return ""
}
}
再実行する
追加実装する場合
基本的にはやることは変わりません。
- テストに追記
- テストFailedを確認
- 処理を実装
- テストの再実行
- テストが通る様になるまで2~4を繰り返す
例) エラーハンドリングを追加する
先ほどの関数に対して、以下の通り期待動作を足します。
string,int,bool以外の型がきた場合はエラーを返却する
テストに追記
package convert
import "testing"
func TestToString(t *testing.T) {
type args struct {
v interface{}
}
tests := []struct {
name string
args args
want string
wantError bool // エラーを期待するか
}{
{name: "String", args: args{v: "hoge"}, want: "hoge", wantError: false},
{name: "Int", args: args{v: 1},want: "1", wantError: false},
{name: "Bool", args: args{v: true}, want: "true", wantError: false},
{name: "Other", args: args{v: 123.0}, want: "", wantError: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ToString(tt.args.v)
// エラーを期待しているにもかかわらず発生しない
if err == nil && tt.wantError {
t.Error("No error occurred.")
return
}
// エラーを期待していないにもかかわらず発生した
if err != nil && !tt.wantError {
t.Error("An error has occurred.")
t.Error(err.Error())
return
}
if got != tt.want {
t.Errorf("ToString() = %v, want %v", got, tt.want)
}
})
}
}
コンパイルを通すために最低限実装
関数の戻り値の数が変更されてしまっているため、コンパイルが落ちてしまいます。
テストを実行するため、最低限実装してあげましょう。
package convert
import (
"strconv"
)
func ToString(v interface{}) (string, error) { // errorを追加
switch vt := v.(type) {
case string:
return vt, nil // とりあえずnilを返却
case int:
return strconv.Itoa(vt), nil
case bool:
return strconv.FormatBool(vt), nil
default:
return "", nil
}
}
テストFailedを確認
エラーを発生させる処理のみテストが落ちています。
それ以外のケースについてはnilで合っているので通ってますね。
処理を実装
package convert
import (
"errors"
"strconv"
)
func ToString(v interface{}) (string, error) {
switch vt := v.(type) {
case string:
return vt, nil
case int:
return strconv.Itoa(vt), nil
case bool:
return strconv.FormatBool(vt), nil
default:
return "", errors.New("Error")
}
}
テストの再実行
無事に通りましたね!
カバレッジをとってみる
ソフトウェア開発において、出来上がったプログラムのテストをする際に、どの程度をテスト対象とする(ことができる)かをカバレッジ(テストカバレッジ)という。
コード量が増えてくると、ちゃんとソース上の各ステップを網羅できてるか確認したくなりますよね。
こちらで実行をすると、以下の通り、カバー率がでます。
もし網羅できていない部分がある場合、ソースの該当箇所が赤くマーキングされて、テストを行っていない箇所が明確になります。
後書き
テストファイルの自動生成が便利ですね。
細かい単位でテストを行うことで、自然とテストが容易なコードを書ける様になり、疎結合なコードを書いていきたくなりますね。