Goで簡単なテスト駆動開発

最近テスト駆動開発を勉強し始めました。
本記事では、Goで簡単なサンプルをテスト駆動開発で実装していきます。

# テスト駆動開発とは
こちらの本でテスト駆動開発を勉強中です。

https://amzn.asia/d/c3oXgaW

テスト駆動開発がどんなものであるか、詳しくは上記の本や別記事を参照いただければと思いますが、簡単に言うと以下を繰り返して開発する手法です。

1. 失敗するテストを書く(この時点ではテストは失敗する)
1. 最小限の変更でテストを通るコードを書く(まずは通れば良い)
1. きれいなコードにする: リファクタリング(テストがパスする状態を維持しながら、意味のあるコードに改善する)

これによって以下のメリットが得られます。

* 仕様への理解が深まる
* 不具合・バグの早期発見につながる
* 開発者の心理的負担が軽減する

# 準備
Goでテスト駆動開発をするための準備をします。

まずはモジュールの作成
```shell
go mod init tdd-sample
```

次に[testify](https://github.com/stretchr/testify)を導入します。
```shell
go get github.com/stretchr/testify
```

# 作るもの
今回作るのは`Cat`クラスです。
> 注) Goにはクラスという概念は無く、構造体やメソッドで**クラスっぽいこと**をします。本記事では便宜上「クラス」と呼びたいと思います。

- `Cat`にはメンバ変数として`sound`がある
- `sound`を返すメソッドとして`Cry()`がある

# Catクラスの作成
それでは`Cat`クラスを作っていきましょう。

...と焦ってはいけません。今回はテスト駆動開発です。
最初にやるべきは**テストを書くこと**です。

まずはテストを書きます。

## 1. 失敗するテストを書く

```go:pet_test.go
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())
}
```

それではテストをしましょう。
```shell
% go test
# tdd-sample_test [tdd-sample.test]
./pet_test.go:10:13: undefined: main.NewCat
FAIL	tdd-sample [build failed]
```

当然、失敗します。というかコンパイルすらできていません。
まずはコンパイルできるようにしましょう。ここで大事なのは**中身はともかくコンパイルできれば良い**ということです。

テスト結果は`NewCat`というメソッドがないと言っています。作りましょう。

```go:cat.go
package main

func NewCat() {
}
```
「中身なにも無くていいの？」と思われるかもしれませんが、小さくステップを踏んでいくことがテスト駆動開発では大事です。
テストを実行しましょう。
```shell
% 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`という構造体(正確にはそのポインタ)を返す

変更を最小限に実装します。
```go:cat.go
package main

type Cat struct {
}

func NewCat(sound string) *Cat {
	return &Cat{}
}
```

テストを実行しましょう。
```shell
% 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`メソッドがないと言っています。実装しましょう。
```go:cat.go
package main

type Cat struct {
}

func NewCat(sound string) *Cat {
	return &Cat{}
}

func (c *Cat) Cry() string {
	return ""
}
```
テストを実行しましょう。
```shell
% 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つ目のステップです。
ここで大事なのは、**とにかくテストを通すこと**です。

前回のテストの結果はこうです。
```shell
Error:      	Not equal:
        	            	expected: "ニャーニャー"
        	            	actual  : ""
```
`pet_test.go`の11行目`assert.Equal(t, "ニャーニャー", c1.Cry())`で、`c1.Cry()`は`"ニャーニャー"`と返してほしかったのですが、実際には空文字`""`を返しています。

このテストを通すのに必要なことは、`c1.Cry()`が`"ニャーニャー"`と返すことです。
それでは、こうしましょう。
```go:cat.go
package main

type Cat struct {
}

func NewCat(sound string) *Cat {
	return &Cat{}
}

func (c *Cat) Cry() string {
	return "ニャーニャー"
}
```
テストを実行します。
```shell
% go test
PASS
ok  	tdd-sample	0.136s
```
やった！！初めてテストが通りました！！

**いやいや、それはあまりに無理やりでは？** と思うかもしれません。

しかしこれで良いのです。これが**2. 最小限の変更でテストを通るコードを書く**のポイントです。
本当は、`sound`というメンバ変数を持って、その中身を`Cry()`で返す…としたいですよね。
焦ってはいけません。それは次の**3. きれいなコードにする: リファクタリング**でやることです。
今は、これでOKです。

## 3. きれいなコードにする: リファクタリング
いよいよコードをきれいにしていきます。
ここでは、少しずつステップを踏んでいきましょう。

まずは、メンバ変数`sound`を作ります。
```go:cat.go
package main

type Cat struct {
    sound string
}

func NewCat(sound string) *Cat {
	return &Cat{}
}

func (c *Cat) Cry() string {
	return "ニャーニャー"
}
```
テストを実行します。(少しの変更でも、毎回テストをするのが大事です。)
まだ通りますね。
続いて、`NewCat`でメンバ変数`sound`に値をセットしましょう。
```go:cat.go
package main

type Cat struct {
    sound string
}

func NewCat(sound string) *Cat {
	return &Cat{sound: "ニャーニャー"}
}

func (c *Cat) Cry() string {
	return "ニャーニャー"
}
```
テストをします。まだ大丈夫。
`Cry`関数で、メンバ変数を返すようにしましょう。
```go:cat.go
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`にも`"ニャーニャー"`が入っています。
これを**重複**といいます。
リファクタリングではこうした重複を除去していきます。

最後に、こうしましょう。
```go:cat.go
package main

type Cat struct {
    sound string
}

func NewCat(sound string) *Cat {
	return &Cat{sound: sound}
}

func (c *Cat) Cry() string {
	return c.sound
}
```
テストをします。
```shell
% go test
PASS
ok  	tdd-sample	0.478s
```
**テストが通りました！**
リファクタリング完了です。

# さいごに
今回は、非常に簡単なお題で、Goを使ったテスト駆動開発を実践しました。
ここで実践したものは、テスト駆動開発のほんの一部です。
まだまだ勉強途中なので、引き続き実践を続けていきます。