最近テスト駆動開発を勉強し始めました。
本記事では、Goで簡単なサンプルをテスト駆動開発で実装していきます。
テスト駆動開発とは
こちらの本でテスト駆動開発を勉強中です。
テスト駆動開発がどんなものであるか、詳しくは上記の本や別記事を参照いただければと思いますが、簡単に言うと以下を繰り返して開発する手法です。
- 失敗するテストを書く(この時点ではテストは失敗する)
- 最小限の変更でテストを通るコードを書く(まずは通れば良い)
- きれいなコードにする: リファクタリング(テストがパスする状態を維持しながら、意味のあるコードに改善する)
これによって以下のメリットが得られます。
- 仕様への理解が深まる
- 不具合・バグの早期発見につながる
- 開発者の心理的負担が軽減する
準備
Goでテスト駆動開発をするための準備をします。
まずはモジュールの作成
go mod init tdd-sample
次にtestifyを導入します。
go get github.com/stretchr/testify
作るもの
今回作るのはCat
クラスです。
注) Goにはクラスという概念は無く、構造体やメソッドでクラスっぽいことをします。本記事では便宜上「クラス」と呼びたいと思います。
-
Cat
にはメンバ変数としてsound
がある -
sound
を返すメソッドとしてCry()
がある
Catクラスの作成
それではCat
クラスを作っていきましょう。
...と焦ってはいけません。今回はテスト駆動開発です。
最初にやるべきはテストを書くことです。
まずはテストを書きます。
1. 失敗するテストを書く
package main_test
import (
"github.com/stretchr/testify/assert"
main "tdd-sample"
"testing"
)
func TestCry(t *testing.T) {
c1 := main.NewCat("ニャーニャー")
assert.Equal(t, "ニャーニャー", c1.Cry())
}
それではテストをしましょう。
% go test
# tdd-sample_test [tdd-sample.test]
./pet_test.go:10:13: undefined: main.NewCat
FAIL tdd-sample [build failed]
当然、失敗します。というかコンパイルすらできていません。
まずはコンパイルできるようにしましょう。ここで大事なのは中身はともかくコンパイルできれば良いということです。
テスト結果はNewCat
というメソッドがないと言っています。作りましょう。
package main
func NewCat() {
}
「中身なにも無くていいの?」と思われるかもしれませんが、小さくステップを踏んでいくことがテスト駆動開発では大事です。
テストを実行しましょう。
% go test
# tdd-sample_test [tdd-sample.test]
./pet_test.go:10:8: main.NewCat("ニャーニャー") (no value) used as value
./pet_test.go:10:20: too many arguments in call to main.NewCat
have (string)
want ()
FAIL tdd-sample [build failed]
以下の2点を指摘されています。
-
(no value) used as value
→NewCat
関数が何もreturnしないため、関数が値として利用されている -
NewCat
関数は引数を取らない
直していきましょう。
ここでそもそもNewCat
関数は何をしたいのでしょうか?
以下2点ですね。
- 鳴き声を引数にとる
-
Cat
という構造体(正確にはそのポインタ)を返す
変更を最小限に実装します。
package main
type Cat struct {
}
func NewCat(sound string) *Cat {
return &Cat{}
}
テストを実行しましょう。
% go test
# tdd-sample_test [tdd-sample.test]
./pet_test.go:11:43: c1.Cry undefined (type *main.Cat has no field or method Cry)
FAIL tdd-sample [build failed]
一歩前進しました!
まだコンパイルエラーはありますがあと一歩です。
次はCry
メソッドがないと言っています。実装しましょう。
package main
type Cat struct {
}
func NewCat(sound string) *Cat {
return &Cat{}
}
func (c *Cat) Cry() string {
return ""
}
テストを実行しましょう。
% go test
--- FAIL: TestCry (0.00s)
pet_test.go:11:
Error Trace: /Users/takasaki.kazunari/work/go-sample/pet_test.go:11
Error: Not equal:
expected: "ニャーニャー"
actual : ""
Diff:
--- Expected
+++ Actual
@@ -1 +1 @@
-ニャーニャー
+
Test: TestCry
FAIL
exit status 1
FAIL tdd-sample 0.136s
ついにコンパイルエラーが無くなりました!
テストは失敗していますが、これで良いです。
1. 失敗するテストを書くが完了しました!
2. 最小限の変更でテストを通るコードを書く
続いて2つ目のステップです。
ここで大事なのは、とにかくテストを通すことです。
前回のテストの結果はこうです。
Error: Not equal:
expected: "ニャーニャー"
actual : ""
pet_test.go
の11行目assert.Equal(t, "ニャーニャー", c1.Cry())
で、c1.Cry()
は"ニャーニャー"
と返してほしかったのですが、実際には空文字""
を返しています。
このテストを通すのに必要なことは、c1.Cry()
が"ニャーニャー"
と返すことです。
それでは、こうしましょう。
package main
type Cat struct {
}
func NewCat(sound string) *Cat {
return &Cat{}
}
func (c *Cat) Cry() string {
return "ニャーニャー"
}
テストを実行します。
% go test
PASS
ok tdd-sample 0.136s
やった!!初めてテストが通りました!!
いやいや、それはあまりに無理やりでは? と思うかもしれません。
しかしこれで良いのです。これが2. 最小限の変更でテストを通るコードを書くのポイントです。
本当は、sound
というメンバ変数を持って、その中身をCry()
で返す…としたいですよね。
焦ってはいけません。それは次の3. きれいなコードにする: リファクタリングでやることです。
今は、これでOKです。
3. きれいなコードにする: リファクタリング
いよいよコードをきれいにしていきます。
ここでは、少しずつステップを踏んでいきましょう。
まずは、メンバ変数sound
を作ります。
package main
type Cat struct {
sound string
}
func NewCat(sound string) *Cat {
return &Cat{}
}
func (c *Cat) Cry() string {
return "ニャーニャー"
}
テストを実行します。(少しの変更でも、毎回テストをするのが大事です。)
まだ通りますね。
続いて、NewCat
でメンバ変数sound
に値をセットしましょう。
package main
type Cat struct {
sound string
}
func NewCat(sound string) *Cat {
return &Cat{sound: "ニャーニャー"}
}
func (c *Cat) Cry() string {
return "ニャーニャー"
}
テストをします。まだ大丈夫。
Cry
関数で、メンバ変数を返すようにしましょう。
package main
type Cat struct {
sound string
}
func NewCat(sound string) *Cat {
return &Cat{sound: "ニャーニャー"}
}
func (c *Cat) Cry() string {
return c.sound
}
テストをします。
OK!順調です。
重複を除去する
ここでNewCat
をよく見てみると、sound
に直接"ニャーニャー"
をセットしています。
テストではc1 := main.NewCat("ニャーニャー")
としているので、引数sound
にも"ニャーニャー"
が入っています。
これを重複といいます。
リファクタリングではこうした重複を除去していきます。
最後に、こうしましょう。
package main
type Cat struct {
sound string
}
func NewCat(sound string) *Cat {
return &Cat{sound: sound}
}
func (c *Cat) Cry() string {
return c.sound
}
テストをします。
% go test
PASS
ok tdd-sample 0.478s
テストが通りました!
リファクタリング完了です。
さいごに
今回は、非常に簡単なお題で、Goを使ったテスト駆動開発を実践しました。
ここで実践したものは、テスト駆動開発のほんの一部です。
まだまだ勉強途中なので、引き続き実践を続けていきます。