このコードラボでは、テスト駆動開発(TDD)という開発手法を使ってFizzBuzzを実装します(FizzBuzzをご存じない方はリンク先でご確認ください)。
テスト駆動開発(TDD)によるソフトウェア開発の流れを学びつつ、Goのテストコードの書き方を学ぶことができます!
このコードラボでは1からソースを作成していきますが、事前に雛形・ディレクトリ構成が用意された以下のサンプルコードをgit clone
または、ZIPでダウンロード・解凍しておくとスムーズに開始できます。
$ git clone https://github.com/DeNA/codelabs.git
このコードラボのサンプルコードはsources/tdd-go
以下に格納されています。
$ cd codelabs/sources/tdd-go $ ls 1_fizzbuzz_start 2_fizzbuzz_answer
ディレクトリ構造は以下のようになっており、1_fizzbuzz_start
がこのコードラボを始める際の雛形になっています。2_fizzbuzz_answer
はこのコードラボを終えたときの最終形のコードが格納されています。
$ tree . . ├── 1_fizzbuzz_start │ └── gopath │ └── src │ └── fizzbuzz │ ├── fizzbuzz.go │ └── fizzbuzz_test.go └── 2_fizzbuzz_answer └── gopath └── src └── fizzbuzz ├── fizzbuzz.go └── fizzbuzz_test.go
最後にGOPATH
を設定し、このコードラボを開始できる状態にしましょう。
$ cd 1_fizzbuzz_start/gopath $ export GOPATH=`pwd`
テスト駆動開発(TDD:Test Driven Development)とは、最初にテストを書き、そのテストがパスする最低限の実装をし、コードを洗練させる(リファクタリング)、というサイクルを繰り返しながらソフトウェアを開発する手法です。
このサイクルは「Red」「Green」「Refactor」という単語で語られます。
Red | 失敗するテストを書く |
Green | テストがパスするコードを書く |
Refactor | コードを洗練させる(リファクタリング) |
TDDではこの Red → Green → Refactor というサイクルをテンポよく繰り返しながら進めていきます。
TDDのメリットとして以下のようなものが挙げられます。
説明はこれくらいにして、実際に手を動かしながらTDDを体験してみましょう!
最初にFizzBuzzで実装すべきことを洗い出しましょう。
"Fizz"
を返す"Buzz"
を返す"FizzBuzz"
を返すこのように最初に『やること』をリストアップするのはTDDでよく行われます。そして、一度にすべてを行おうとせず一つずつ消化をしていきます。
TDDは短時間で Red → Green → Refactor のサイクルを繰り返すため、最終的に何をやるのか忘れてしまいがちです。それを防ぐためにも最初にやることをリストアップするのは良い習慣と言えるでしょう。
このコードラボでは上から順番に実装を進めていきます。
まずはテストコードの雛形としてfizzbuzz_test.go
を作成します。
package fizzbuzz_test
import (
"testing"
)
func TestFizzBuzz(t *testing.T) {
}
Go言語ではファイル名の末尾に_test
をつけたものがテストコードとして認識されます。ここではテスト対象のファイル名をfizzbuzz.go
にするつもりでfizzbuzz_test.go
というファイル名にしました。
パッケージ名も同様に末尾に_test
をつけています。Go言語では、他のパッケージから利用される想定の関数に対するテストでは、このようにすることで別パッケージとして扱うことができます。
Go言語では標準パッケージにtesting
というテスト用のパッケージが含まれています。
TestFizzBuzz
という関数を作成しましたが、これがテスト用の関数になります。Go言語では次のルールにあてはまるものがテスト用の関数として認識されます。
Test
という単語から始まる(先頭は大文字である必要がある)*testing.T
を受け取るまだ関数の中身はありませんが一度テストを実行してみましょう。fizzbuzz
パッケージのディレクトリに移動し、ターミナルからgo test
コマンドでテストを実行できます。
$ cd src/fizzbuzz $ go test -v === RUN TestFizzBuzz --- PASS: TestFizzBuzz (0.00s) PASS
うまく実行できれば最後にPASS
という単語が出力されます。-v
オプションは必須ではありませんが、これを付けることでどのテストが実行されたか分かるようになります。
これでテストを書いて実行できる環境が整いました。次のステップから実際にTDDで実装を進めていきます!
それではTDDの最初のステップである『失敗するテストを書く』ところから始めましょう。
ここでのポイントは自分が欲しいインターフェースを考えることです。今回はint
を渡してstring
を返すインターフェースが欲しいと考え、関数名はシンプルにFizzbuzz
としてみました。
// fizzbuzz_test.go
package fizzbuzz_test
import (
"testing"
"fizzbuzz" // インポートを追加
)
func TestFizzBuzz(t *testing.T) {
got := fizzbuzz.FizzBuzz(1)
if got != "1" {
t.Errorf(`FizzBuzz(1) is %q`, got)
}
}
ここでは関数の呼び出し結果を変数got
に格納し、それが期待値である"1"
と一致しなかった場合にテストを失敗させるようにしています。
このようにGo言語ではユニットテストにおいて幅広く利用されているxUnitフレームワークとは異なりアサーション関数は用意されていません。そのかわりに期待しない結果が得られた場合に、テストが失敗したことを記録する関数(ここではtestingパッ
ケージのT.Errorf関数)を呼びます。
さてFizzBuzz
という関数は作成していないので現状ではコンパイルは通りません。テストを実行してそれを確認してみましょう。
$ go test -v ./fizzbuzz_test.go:8:7: undefined: fizzbuzz
まずはこれを修正することをだけを考えて、プロダクトコードを実装していきます。fizzbuzz.go
というファイル名でソースを作成し、FizzBuzz
関数の雛形を実装します。
// fizzbuzz.go
func FizzBuzz(n int) string {
return ""
}
中身の実装は空文字列を返すだけのダミー実装ですが問題ありません。今はコンパイルを通すことが目的だからです。
この状態でテストを実行すると次のようにテストが失敗することが確認できます。
$ go test -v === RUN TestFizzBuzz --- FAIL: TestFizzBuzz (0.00s) fizzbuzz_test.go:11: FizzBuzz(1) is "" # Errorf関数による出力 FAIL exit status 1 FAIL github.dena.jp/swet/go-codelabs/tdd/fizzbuzz 0.018s
FizzBuzz(1) is ""
という出力がされていますが、これはT.Errorf
関数に渡した引数によるものです。このように『テストが失敗したときの正確な状況』を把握できるようにするため適切な値を設定することが大切です。
これで最初の『失敗するテストを書く(Red)』が出来ました!
次のステップは『テストがパスする最小限のコードを書く』です。
今回のテストをパスさせる最小限の実装は何でしょうか。それは文字列"1"
を返すことです。
// fizzbuzz.go
func FizzBuzz(n int) string {
return "1" // "1"を返すように修正
}
この状態でテストを実行すると、確かにテストがパスすることがわかります。
$ go test -v === RUN TestFizzBuzz --- PASS: TestFizzBuzz (0.00s) PASS
このようにテストがパスするように値をハードコーディングすることをTDDではFake It(仮実装)と呼びます。TDDではこのように確実なことを少しずつ進めることで、自信をたもちながら開発していく方法がよく使われます。
これで最初の『テストがパスする最小限のコードを書く(Green)』が出来ました!
最後のステップは『コードをきれいにする』です。
コードをきれいにする行為は一般的にリファクタリングという単語で知られています。このコードラボでも以降はリファクタリングという単語を使用します。
さて現状でリファクタリングすべき箇所はあるでしょうか?
テストコードをあらためて見てみます。
// fizzbuzz_test.go
func TestFizzBuzz(t *testing.T) {
got := fizzbuzz.FizzBuzz(1)
if got != "1" {
t.Errorf(`FizzBuzz(1) is %q`, got)
}
}
fizzbuzz
パッケージのFizzBuzz
関数を呼ぶというインターフェースになっていますが、単語が重複していて冗長な印象を受けます。ここではfizzbuzz.Convert(1)
と呼べるほうが直感的であると感じたのでリファクタリングしてみます。
// fizzbuzz_test.go
func TestConvert(t *testing.T) {
got := fizzbuzz.Convert(1)
if got != "1" {
t.Errorf(`Convert(1) is %q`, got)
}
}
関数名やエラーメッセージをConvert
に変更し、それにあわせてテスト用の関数も名前を変更しました。
この状態でテストを実行すると「fizzbuzz.Convert
が定義されていない」というコンパイルエラーとなり、期待通りにテストが失敗していることが確認できます。
./fizzbuzz_test.go:9:9: undefined: fizzbuzz.Convert
コンパイルエラーを解消するために、プロダクトコードの方も変更します。
// fizzbuzz.go
func Convert(n int) string {
return "1"
}
コード変更が終わったらテストを実行し、コードを壊していないことを確認しましょう。
$ go test -v === RUN TestFizzBuzz --- PASS: TestFizzBuzz (0.00s) PASS
問題なさそうですね!
このようにテストコードが用意されていることで、自信を持ってリファクタリングできます。
これでTDDのサイクルである Red / Green / Refactor を体験できました!
ここまでで数字の1
を渡したときに文字列"1"
が返されるところまで進められました。しかし、他の数字、例えば2
を渡した時にFizzBuzzの仕様どおりに動くのでしょうか?
ためしにテストコードを追加してみましょう。
// fizzbuzz_test.go
func TestConvert(t *testing.T) {
got := fizzbuzz.Convert(1)
if got != "1" {
t.Errorf(`Convert(1) is %q`, got)
}
// このテストを追加
got = fizzbuzz.Convert(2)
if got != "2" {
t.Errorf(`Convert(2) is %q`, got)
}
}
これでテストを実行してみると失敗することがわかります。
$ go test -v === RUN TestConvert --- FAIL: TestConvert (0.00s) fizzbuzz_test.go:16: Convert(2) is "1"
現状では"1"
を固定で返却していたので、テストがパスするように修正してみます。strconv.Itoa
関数を利用することで数値から文字列への変換が行えるのでそれを利用します。
// fizzbuzz.go
import (
"strconv"
)
func Convert(n int) string {
return strconv.Itoa(n)
}
テストが成功することを確認しましょう。
$ go test -v === RUN TestFizzBuzz --- PASS: TestFizzBuzz (0.00s) PASS
このようにあとから別のデータを追加し、それらのテストがパスするように本来あるべきコードに修正する方法を三角測量と呼びます。
これで最初のTODOが消化できました!
"Fizz"
を返す"Buzz"
を返す"FizzBuzz"
を返す残りのTODOも順番に消化していきましょう。
"Fizz"
を返す"Buzz"
を返す"FizzBuzz"
を返す3
で割り切れるときは"Fizz"
という文字列を返す実装を進めていきましょう。
まずはテストコードを追加します。
// fizzbuzz_test.go
func TestConvert(t *testing.T) {
got := fizzbuzz.Convert(1)
if got != "1" {
t.Errorf(`Convert(1) is %q`, got)
}
got = fizzbuzz.Convert(2)
if got != "2" {
t.Errorf(`Convert(2) is %q`, got)
}
// このテストを追加
got = fizzbuzz.Convert(3)
if got != "Fizz" {
t.Errorf(`Convert(3) is %q`, got)
}
}
この状態でテストを実行すると失敗するはずです。確認してみましょう。
$ go test -v === RUN TestConvert --- FAIL: TestConvert (0.00s) fizzbuzz_test.go:16: Convert(3) is "3"
このようにあえて失敗させるのは自分が書いたテストコードが正しいことを確認するという意味があります。
TDDによる実装を始める前にテストの雛形だけ用意してテストを実行した時、テストが成功したのを覚えているでしょうか?これはテストが成功したとしても、正しくテストできているという保証はないことを意味します。
テストがパスした時に「ちゃんとテスト出来ているのだろうか?」という不安に陥らないように、テストが期待どおり失敗することを確認するのです。
テストがパスするように実装していきます。
// fizzbuzz.go
func Convert(n int) string {
if n%3 == 0 {
return "Fizz"
}
return strconv.Itoa(n)
}
前回は仮実装(Fake It)を利用して固定値を返すようにしましたが、ここでは自信があったので最初から本来のロジックを実装しました。このように正解が明らかで不安がないと自分が感じる場合に最初から実装することを明白な実装と呼びます。
その自信のとおりに正しく実装できているのでしょうか、テストを実行して確認してみましょう。
$ go test -v === RUN TestConvert --- PASS: TestConvert (0.00s) PASS
問題ないことを確認できたら次に進みましょう。
この段階でリファクタリングすべき箇所はあるでしょうか?
テストコードに重複があるのでリファクタリング候補ではありますが、現状では十分にシンプルにも感じます。今回はこのままにして次のステップに進むことにします。
これで2つ目のTODOが消化できました。
"Fizz"
を返す"Buzz"
を返す"FizzBuzz"
を返す次は5
で割り切れるときは"Buzz"
という文字列を返す実装です。
これまで同様に失敗するテストコードを追加します。
// fizzbuzz_test.go
func TestConvert(t *testing.T) {
got := fizzbuzz.Convert(1)
if got != "1" {
t.Errorf(`Convert(1) is %q`, got)
}
got = fizzbuzz.Convert(2)
if got != "2" {
t.Errorf(`Convert(2) is %q`, got)
}
got = fizzbuzz.Convert(3)
if got != "Fizz" {
t.Errorf(`Convert(3) is %q`, got)
}
// このテストを追加
got = fizzbuzz.Convert(5)
if got != "Buzz" {
t.Errorf(`Convert(5) is %q`, got)
}
}
テストが失敗することを確認できたら進みましょう。
テストがパスするコードを実装していきます。
今回も自信があるので「明白な実装」をしました。
// fizzbuzz.go
func Convert(n int) string {
// これを追加
if n%5 == 0 {
return "Buzz"
}
if n%3 == 0 {
return "Fizz"
}
return strconv.Itoa(n)
}
テストがパスすることが確認できたら進みましょう。
さきほどリファクタリングを見送ったテストコードの重複ですが、このままテストパターンが増えていくことを考えると冗長だと感じます。このタイミングでリファクタリングしてみましょう。
Go言語ではテーブル駆動テストという手法が多く利用されます。事前にテストケース(入力データ、期待値)を用意しておき、それを順番に繰り返しテストするという考え方です。
現状のテストコードを次のように書き換えてみましょう。
// fizzbuzz_test.go
func TestConvert(t *testing.T) {
tests := []struct {
n int // 入力値
want string // 期待値
} {
{ n: 1, want: "1" },
{ n: 2, want: "2" },
{ n: 3, want: "Fizz" },
{ n: 5, want: "Buzz" },
}
for _, tt := range tests {
got := fizzbuzz.Convert(tt.n)
if got != tt.want {
t.Errorf(`Convert(%v) = %q but want %q`, tt.n, got, tt.want)
}
}
}
テストとして行っていることは変わりませんが、事前にテストパターンが列挙される形式になり見通しがよくなりました。
ここでもテストを実行して、リファクタリングによって何かを壊していないか確認してみましょう。
$ go test -v === RUN TestConvert --- PASS: TestConvert (0.00s) PASS ...
テストがパスしたので一安心ですが、そもそも全パターンのテストが実行されているのかという不安はないでしょうか。例えば、ループ処理がうまく書けていなかった場合は十分ありえる話です。
その不安を取り除くために、テストデータをあえて失敗するはずの値に変更してテストを実行してみます。期待値であるwant
の先頭にx
を追加してテストを実行してみます。
// fizzbuzz_test.go
func TestConvert(t *testing.T) {
tests := []struct {
n int
want string
} {
{ n: 1, want: "x1" }, // 先頭にxを追加
{ n: 2, want: "x2" },
{ n: 3, want: "xFizz" },
{ n: 5, want: "xBuzz" },
}
for _, tt := range tests {
got := fizzbuzz.Convert(tt.n)
if got != tt.want {
t.Errorf(`Convert(%v) = %q but want %q`, tt.n, got, tt.want)
}
}
}
すべてのテストケースが実行されていれば、そのようにレポートされるはずです。
$ go test -v === RUN TestConvert --- FAIL: TestConvert (0.00s) fizzbuzz_test.go:22: Convert(1) = "1" but want "x1" fizzbuzz_test.go:22: Convert(2) = "2" but want "x2" fizzbuzz_test.go:22: Convert(3) = "Fizz" but want "xFizz" fizzbuzz_test.go:22: Convert(5) = "Buzz" but want "xBuzz" FAIL
期待どおり4つのテストすべてが失敗しています!
このように不安な部分があればひとつずつ確認して進むというのもTDDでは大切とされています。今回あえてテストを失敗させましたがこれはTDDサイクルにおけるRedではなく、不安を取り除くために自発的におこなったものです。
これで不安を取り除けたので、テストデータをもとに戻して最後の実装に進みます。
"Fizz"
を返す"Buzz"
を返す"FizzBuzz"
を返す最後に3
の5
の両方で割り切れるときは"FizzBuzz"
という文字列を返す実装です。
いつもどおり失敗するテストの追加から始めます。
さきほどのリファクタリングによりテストの追加はとても簡単です。
// fizzbuzz_test.go
func TestConvert(t *testing.T) {
tests := []struct {
n int
want string
} {
{ n: 1, want: "1" },
{ n: 2, want: "2" },
{ n: 3, want: "Fizz" },
{ n: 5, want: "Buzz" },
{ n: 15, want: "FizzBuzz" },
}
for _, tt := range tests {
got := fizzbuzz.Convert(tt.n)
if got != tt.want {
t.Errorf(`Convert(%v) = %q but want %q`, tt.n, got, tt.want)
}
}
}
テストが失敗することを確認できたら次に進みます。
実装を追加してテストがパスすることを確認します。
// fizzbuzz.go
func Convert(n int) string {
if n%3 == 0 && n%5 == 0 {
return "FizzBuzz"
}
if n%5 == 0 {
return "Buzz"
}
if n%3 == 0 {
return "Fizz"
}
return strconv.Itoa(n)
}
そろそろこのリズムにも慣れてきた頃ではないでしょうか?
このタイミングでリファクタリングすべき箇所はあるでしょうか?
実装側のコードを見てみるとif
の羅列になっているので、Go言語のswitch
文を利用するとスッキリかけそうな気がします。やってみましょう。
// fizzbuzz.go
func Convert(n int) string {
switch {
case n%3 == 0 && n%5 == 0:
return "FizzBuzz"
case n%5 == 0:
return "Buzz"
case n%3 == 0:
return "Fizz"
default:
return strconv.Itoa(n)
}
}
テストを実行してリファクタリングが成功していることを確認しましょう。
$ go test -v === RUN TestConvert --- PASS: TestConvert (0.00s) PASS
問題なさそうです。
ところで最初の条件文であるn%3 == 0 && n%5 == 0
ですが、3
と5
の両方で割り切れるということは15
で割り切れることと同義です。ここもあわせてリファクタリングしましょう。
// fizzbuzz.go
func Convert(n int) string {
switch {
case n%15 == 0: // ここを変更
return "FizzBuzz"
case n%5 == 0:
return "Buzz"
case n%3 == 0:
return "Fizz"
default:
return strconv.Itoa(n)
}
}
簡単な変更ですが、念のためテストを実行して壊していないことを確認しましょう。
これですべてのタスクを消化し、FizzBuzzの実装を終えることができました!
"Fizz"
を返す"Buzz"
を返す"FizzBuzz"
を返すところで現状は1
、2
、3
、5
、15
の計5つのパターンしかテストしていません。これだけのテストパターンで不安はないでしょうか?とくにFizzとBuzzは割り切れる最初の値(3
と5
)しかテストしていないことがわかります。
さすがにこれだけでは不安だと感じるのでテストパターンを追加していきます。
今回は1〜15までを確認できれば十分安心だと思い、次のようにテストパターンを追加してテストを実行し、不安を解消しましょう!
func TestConvert(t *testing.T) {
tests := []struct {
n int
want string
} {
{ n: 1, want: "1" },
{ n: 2, want: "2" },
{ n: 3, want: "Fizz" },
{ n: 4, want: "4" },
{ n: 5, want: "Buzz" },
{ n: 6, want: "Fizz" },
{ n: 7, want: "7" },
{ n: 8, want: "8" },
{ n: 9, want: "Fizz" },
{ n: 10, want: "Buzz" },
{ n: 11, want: "11" },
{ n: 12, want: "Fizz" },
{ n: 13, want: "13" },
{ n: 14, want: "14" },
{ n: 15, want: "FizzBuzz" },
}
...
不安を解消できたら次に進みましょう!
現状でも十分なレベルに仕上がっていますが、最後にテストコードを少し改善して終わりにしましょう。
今のテストコードでは1〜15の入力パターンに対してテストを行っていますが、コンソール上では1つのテストとして扱われていました。t.Run
メソッドを利用するとそれらを個別のテストとして実行することが出来ます。
次のようにテストコードを変更してみましょう。
import (
"fmt" // fmtパッケージを追加
...
}
func TestConvert(t *testing.T) {
...
for _, tt := range tests {
name := fmt.Sprintf("number:%v", tt.n) // テストの名前
// サブテストとして実行
t.Run(name, func(t *testing.T) {
got := fizzbuzz.Convert(n)
if got != tt.want {
t.Errorf(`Convert(%v) = %q but want %q`, tt.n, got, tt.want)
}
})
}
}
t.Run
メソッドは、第1引数がテストケース名、第2引数がテストとして実行する関数となっています。
この状態でテストを実行すると次のような出力が得られます。
$ go test -v === RUN TestConvert === RUN TestConvert/number:1 === RUN TestConvert/number:2 === RUN TestConvert/number:3 === RUN TestConvert/number:4 === RUN TestConvert/number:5 === RUN TestConvert/number:6 === RUN TestConvert/number:7 === RUN TestConvert/number:8 === RUN TestConvert/number:9 === RUN TestConvert/number:10 === RUN TestConvert/number:11 === RUN TestConvert/number:12 === RUN TestConvert/number:13 === RUN TestConvert/number:14 === RUN TestConvert/number:15 --- PASS: TestConvert (0.00s) --- PASS: TestConvert/number:1 (0.00s) --- PASS: TestConvert/number:2 (0.00s) --- PASS: TestConvert/number:3 (0.00s) --- PASS: TestConvert/number:4 (0.00s) --- PASS: TestConvert/number:5 (0.00s) --- PASS: TestConvert/number:6 (0.00s) --- PASS: TestConvert/number:7 (0.00s) --- PASS: TestConvert/number:8 (0.00s) --- PASS: TestConvert/number:9 (0.00s) --- PASS: TestConvert/number:10 (0.00s) --- PASS: TestConvert/number:11 (0.00s) --- PASS: TestConvert/number:12 (0.00s) --- PASS: TestConvert/number:13 (0.00s) --- PASS: TestConvert/number:14 (0.00s) --- PASS: TestConvert/number:15 (0.00s) PASS
今回のFizzBuzzでは単なる連番であるためそこまで重要性を感じないかもしれませんが、どういったパターンがテストされているのか分かるのは便利です。
今回のテストパターンはそれぞれを独立して実行させても問題ありません。
以下のようにテストコードを変更して並列実行させてみましょう。
func TestConvert(t *testing.T) {
...
for _, tt := range tests {
tt := tt // ローカル変数を用意
name := fmt.Sprintf("number:%v", tt.n)
t.Run(name, func(t *testing.T) {
t.Parallel() // 並列実行するように
got := fizzbuzz.Convert(tt.n)
if got != tt.want {
t.Errorf(`Convert(%v) = %q but want %q`, tt.n, got, tt.want)
}
})
}
}
t.Run
メソッドに与えている関数がループ毎のtt
をキャプチャできるようにループの先頭でローカル変数を用意しています。
また、t.Run
メソッド内でt.Parallel
メソッドを呼び出し並列実行するように指定しています。
このようにすることでテストが並列化されて実行されるようになります。
なおデフォルトではGOMAXPROCS
の数(デフォルトではCPUのコア数)で並列化されますが、-parallel n
オプションを実行時に与えることでnの数で並列化が行われるようになります。次は並列数として2を指定する例です。
$ go test -v -parallel 2
最後にt.Run
メソッド内で記述しているテスト用のコードをメソッドとして抽出して見通しを良くしてみましょう。
次のようにテストコードを変更します。
func TestConvert(t *testing.T) {
...
for _, tt := range tests {
tt := tt
name := fmt.Sprintf("number:%v", tt.n)
t.Run(name, func(t *testing.T) {
t.Parallel()
testFizzBuzz(t, tt.n, tt.want) // 関数を呼び出すように変更
})
}
}
// テスト関数として抽出
func testFizzBuzz(t *testing.T, n int, want string) {
t.Helper()
got := fizzbuzz.Convert(n)
if got != want {
t.Errorf(`Convert(%v) = %q but want %q`, n, got, want)
}
}
testFizzBuzz
関数の先頭でt.Helper
メソッドを呼び出していますが、これはテストが失敗した時に報告されるエラー行をtestFizzBuzz
関数の呼び出し元にする効果があります。これを呼び出さなかった場合にテストが失敗すると、報告されるエラー行は以下のようにtestFizzBuzz
関数内でt.Errorf
メソッドを呼び出している箇所になってしまいます。
--- FAIL: TestConvert/number:1 (0.00s) fizzbuzz_test.go:47: Convert(1) = "1" but expect "Foo" # # t.Helperを利用しない場合、t.Errorf関数の位置で失敗したと報告される # t.Errorf(`Convert(%v) = %q but want %q`, n, got, want) # L47
t.Helper
メソッドを利用した場合、testFizzBuzz
関数の呼び出し元で失敗したと報告されます。
--- FAIL: TestConvert/number:1 (0.00s) fizzbuzz_test.go:38: Convert(1) = "1" but expect "Foo" # # t.Helperを利用した場合、testFizzBuzz関数の位置で失敗したと報告される # testFizzBuzz(t, tt.n, tt.expect) #L38
最後にテストを実行して、問題なくパスすることを確認しましょう。
このコードラボを通じて、TDDのサイクルである Red / Green / Refactor を何度も回しながらFizzBuzzを完成させました!
実装コード・テストコードともに見通しがよくクリーンな状態になっていますし、自動化されたテストコードのおかげで仕様変更やリファクタリングも自信をもって行うことができるでしょう。
testing
パッケージを利用したテストの書き方を学んだそれでは、よきTDDライフを!
今回の題材はさすがに簡単すぎて退屈だったでしょうか?あるいは物足りないでしょうか?
TDDBC(TDD Bootcamp)というイベントで題材として使われている、ポーカーをTDDで実装してみるのも面白いでしょう。