XP一日体験ワークショップに参加+α
XP1日体験ワークショップなるものに参加してきました。
で、どういうことがあったかとかそのあたりの感想はきっと誰かが書いてくれるはずなので、簡単に感想を書いておきます。
- devstは2回目だが、この勉強会はいつも面白い試みをしているなと感じる
- 決まった運営というものがないので仕方がないのかもしれないが、も少しテンポよくできればいいかも?
- お絵描きがかなり苦手な私としましては、ペアドローはかなりきついですね…
- 直接は関係ないが、この手の勉強会で言語を"Javaに"固定するのは厳しいのかなと再認識
- コミットしたらWebページでテスト結果が見られるのは面白かった
- XPの内容をいくつかまとめてやろうとするには、今回はちょっと時間がたりなかった感がある(CIははずしたほうがよかったのでは)
- ペアの方にEclipseのQuick JUnit Plugin、git-nowあたりをおすすめしてみた
- この手のイベントはサポーターいりますね、と再認識
- Mavenェ…Eclipseェ…
と、短い感想だけでは寂しいのでちょっとネタなことを書いていこうと思います。そもそもネタな上に議論中のネタまで入っているのでかなりアレな内容になっています。
繰り返しますがネタです。
ATDDみたいなの
世の中にはAcceptance Test Driven Development(またはシナリオBDD?)というものが存在します。
1サイクルの作業は確か次の形をとるはずです。
- 失敗する受け入れテストを書く(Acceptance Test Driven phase Red)
- 失敗するテストを書く (Test Driven phase Red)
- テストにとおる最小の実装を行う (Test Driven phase Green)
- リファクタリング (Test Driven phase Refactor)
- 受け入れテストにとおることを確認する (Acceptance Test Driven phase Green)
- 受け入れテストのリファクタリング (Acceptance Test Driven phase Refactor)
Acceptance Testを先に書くこと以外に特に目新しいことはありません。実装部分はいつもどおりTDDすることになります。
specs2(Scala)で受け入れてストを書く
JVMな言語は便利なもので、他のJVMな言語で実装されたプログラムもJVM上であれば同じように動作します。
というわけで今回は、、お試しとしてAcceptance TestをScalaで書いてみましょう!
今回はspece2というテスティングフレームワークを使います。specs2は皆さんがよく書くようなテストコードもかけますが、受入テストの形式でも記述が可能なフレームワークです。
今回は僕らの永遠のデモお題"FizzBuzz"を実装していきます。
まずは受け入れテストを書きましょう。
package judges import org.specs2._ import specification._ import features.rabbit.FizzBuzz class FizzBuzzAcceptanceSpec extends Specification { def is = "FizzBuzzのテスト" ^ p^ "3の倍数について" ^ br^ "${3}を渡すと" ^ number^ "${Fizz}を返す" ^ fizzbuzz^ end^ t^ "${18}を渡すと" ^ number^ "${Fizz}を返す" ^ fizzbuzz^ end object number extends Given[Int] { def extract(text: String) = extract1(text).toInt } object fizzbuzz extends Then[Int] { def extract(number: Int, text: String) = sut.fizzBuzz(number) must_== extract1(text) } val sut = new FizzBuzz
FizzBuzz受け入れテストいるのかってほどに小さいので、受け入れテストの粒度もかなり小さくなってしまいますね。仕方がないので今回は3の倍数の値できちんとFizzが返ってくることを1つのシナリオにしました。本当はもっと粒度をシナリオレベルにしないといけません。
あと「この受け入れテストはFizzBuzzという実装クラスに依存しているけどインターフェイスじゃないのー?」とかそのあたりの問題は今回は割愛。わりと面倒なので。
実行するともちろん失敗します。
spock(Groovy)でユニットテストを書く
それではユニットテストを書いていきましょう!
今回はspockというテスティングフレームワークでテストを記述していきます。
package features.rabbit import spock.lang.Specification class FizzBuzzTest extends Specification { def "3を渡すとFizzが返る" () { given: def sut = new FizzBuzz() expect: sut.fizzBuzz(3) == 'Fizz' } }
3を渡したらFizzが返ってくることをテストですが、もちろんここで実行しようとしてもクラスがないのでビルドエラーになります。
仮実装(Java)
テスト対象がないことにはテストもできません。とりあえずクラスを作りましょう。
package features.rabbit; public class FizzBuzz implements features.FizzBuzz { @Override public String fizzBuzz(int i) { return null; } }
まだテストは落ちます。そりゃそうですね、IDEに自動生成してもらっただけですから!
Greenにしましょう。えい、やっ。
package features.rabbit; public class FizzBuzz implements features.FizzBuzz { @Override public String fizzBuzz(int i) { return "Fizz"; } }
returnをnullから"Fizz"に書き換えました。これでテストに通るようになりますが、これは何をしているのかというと、仮実装といって簡単な実装をすることでテストコードのテストをしています。こんな簡単なコードのテストが通らないということは環境に問題があるということなのです。
ちなみに受け入れテストはまだ通りません。
三角測量
受け入れテストを見るに、まだ3の倍数のときの用件が満たせていません。
それはそうだ、Fizz返しているだけだもの。というわけで別の値でテストしてみましょう。TDDでは三角測量と呼ばれているやつですね。
package features.rabbit import spock.lang.Specification class FizzBuzzTest extends Specification { def "3を渡すとFizzが返る" () { given: def sut = new FizzBuzz() expect: sut.fizzBuzz(3) == 'Fizz' } def "18を渡すとFizzが返る" () { given: def sut = new FizzBuzz() expect: sut.fizzBuzz(18) == 'Fizz' }
テストに失敗しました。仮実装のコードはこんなもんです。
いつまでも安直なのもあれなので、実装。
package features.rabbit; public class FizzBuzz implements features.FizzBuzz { @Override public String fizzBuzz(int i) { if(i % 3 == 0) return "Fizz"; return null; } }
やったー、テストにとおったー
でもってリファクタリング
テストコードに重複が見られるので、重複をなくしていきましょー。
package features.rabbit import spock.lang.Specification class FizzBuzzTest extends Specification { def "3の倍数ならFizzが返る" () { given: def sut = new FizzBuzz() expect: sut.fizzBuzz(num) == result where: num | result 3 | 'Fizz' 15 | 'Fizz' } }
パラメータ化テストの形式を使うとすっきりしますね。
受け入れテストに通ることを確認
ここで受け入れテストを実行すると、テストに通ってGreenになります。
受け入れテストのリファクタリング
リファクタリングのしようがないので、やりません。が、受け入れテストをリファクタリングしたい場合はこのタイミングで行います。
今回はFizzBuzzなのでちょっとよくわからないかもしれませんが、通常は1シナリオの粒度はユニットテストよりも大きいので、受け入れテストの粒度>ユニットテストの粒度になるはずです。つまりATDDのサイクル内には複数回のTDDサイクルがあることになります。
どんどんまわす
さて、どんどんATDDのサイクルをまわしていくと以下のようなコードになりました。
package judges import org.specs2._ import specification._ import features.rabbit.FizzBuzz class FizzBuzzAcceptanceSpec extends Specification { def is = "FizzBuzzのテスト" ^ p^ "3の倍数について" ^ br^ "${3}を渡すと" ^ number^ "${Fizz}を返す" ^ fizzbuzz^ end^ t^ "${18}を渡すと" ^ number^ "${Fizz}を返す" ^ fizzbuzz^ endp^ "5の倍数について" ^ br^ "${5}を渡すと" ^ number^ "${Buzz}を返す" ^ fizzbuzz^ end^ t^ "${35}を渡すと" ^ number^ "${Buzz}を返す" ^ fizzbuzz^ endp^ "15の倍数について" ^ br^ "${15}を渡すと" ^ number^ "${FizzBuzz}を返す" ^ fizzbuzz^ end^ t^ "${150}を渡すと" ^ number^ "${FizzBuzz}を返す" ^ fizzbuzz^ endp^ "その他の値について" ^ br^ "${4}を渡すと" ^ number^ "そのまま値を返す" ^ fizzbuzzValue^ end^ t^ "${7}を渡すと" ^ number^ "そのまま値を返す" ^ fizzbuzzValue^ endp^ "0と負数について" ^ br^ "${0}を渡すと" ^ number^ "実行時例外を送出する" ^ throwException^ end^ t^ "${-3}を渡すと" ^ number^ "実行時例外を送出する" ^ throwException^ end object number extends Given[Int] { def extract(text: String) = extract1(text).toInt } object fizzbuzz extends Then[Int] { def extract(number: Int, text: String) = sut.fizzBuzz(number) must_== extract1(text) } object fizzbuzzValue extends Then[Int] { def extract(number: Int, text: String) = sut.fizzBuzz(number) must_== number.toString } object throwException extends Then[Int] { def extract(number: Int, text: String) = sut.fizzBuzz(number) must throwA[RuntimeException] } val sut = new FizzBuzz }
package features.rabbit import spock.lang.Specification class FizzBuzzTest extends Specification { def sut def setup() { sut = new FizzBuzz() } def "与えられた数値に対して期待するFizzBuzzの結果が返る" () { expect: sut.fizzBuzz(num) == result where: num | result 3 | 'Fizz' 5 | 'Buzz' 15 | 'FizzBuzz' 7 | '7' } def "0以下の値が渡された場合はRuntimeExceptionをなげる" () { when: sut.fizzBuzz(num) then: thrown(RuntimeException) where: num << [0, -1] } }
package features.rabbit; public class FizzBuzz implements features.FizzBuzz { @Override public String fizzBuzz(int i) { if(i <= 0) throw new RuntimeException("FizzBuzz:引数に0以下の値が渡されました"); if(i % 3 == 0 && i % 5 == 0) return "FizzBuzz"; if(i % 5 == 0) return "Buzz"; if(i % 3 == 0) return "Fizz"; return String.valueOf(i); } }
なんともまぁ、JVM言語駆動な開発ですことね。一応GitHubにあげておいたので今後も色々といじってみます。
pocketberserker/devst_awaji_rabbit · GitHub
なぜ今回はプロダクトコードとテストコードの言語が別なの?
ことJavaに関しては、個人的にテストコードが書きづらいと思っております。で、JVM上なら同じ動作になるのでJVM上の言語でならJavaのコードをテストしても問題ないかなとか考えてます。
あともう一つ個人的にですが、ユニットテストの話であれば、別言語で書いたユニットテストコードはある程度"捨てる前提"で書いてます。要するにローカル内にだけ存在する形を想定していたりします。私はあとから単体テスト(notユニットテスト)を作ったりするタイプなので、後で作り直したほうを公開するとかしないとか。
まぁ、テストはドキュメントだから同じ言語で書くべきだとか、チーム全員が知っているわけではないんだぞ、とかそういう話はあるので考え方は人それぞれです。それぞれというかまだ議論があまり決着していない気もする。
個人的には、他人やチームに迷惑をかけない範囲で、かつ同一実行環境上でテストできる言語なら書きやすいほうを選びたいよね、と思える人ならこの方法もありなのではないかと考えております。
ビルド環境とか
構成はこんな感じでやってました(devstのデータを流用)。
project_root ├── pom.xml -> 残骸 ├── build.gradle -> ビルド設定ファイル(TDD用) ├── build.sbt -> ビルド設定ファイル(受け入れテスト用) ├── build -> gradleが生成するディレクトリ ├── project ├── plugin.sbt -> sbtプロジェクト設定ファイル └── src -> ソースコードのディレクトリ ├── main -> プロダクト用ディレクトリ │ └── java -> java用 └── test -> テスト用ディレクトリ ├── scala -> scala用 └── groovy -> groovy用
プロジェクトはGitで管理し、受け入れテスト用のファイル群はacceptanceブランチ、開発用のコードはmasterやトピックブランチで管理していました。
おわりに
XP1日体験ワークショップ楽しかったです。関係者の皆様、ありがとうございました。