TDD Hands-on
-
Upload
shuji-watanabe -
Category
Technology
-
view
1.702 -
download
2
description
Transcript of TDD Hands-on
TDD Boot CampIntroduction
Pre TDD Boot Campへようこそ。この資料は、TDD Boot Campを安心して受講できるようにTDD(テスト駆動開発)の雰囲気をハンズオン形式で学ぶ事を目的としています。このハンズオンを通してTDDを体験してみましょう。
それぞれのExerciseには目安となる所要時間が記述されています。ワークショップではこの時間を目安に次のExerciseへと進み、約半日で最後まで終わるように計画されています。尚、各Exerciseでは早く終了した人の為にOptionが用意されています。
ハンズオンに参加される方は、必要に応じてこの資料を印刷して持参してください(当日は印刷物の提供はありません)。また、JDKとEclipseに関しては予めセットアップの上でご参加ください。
Overview
このハンズオンで作成していくアプリケーションはWiki Engineで使用する事を想定したWiki Formatterです。特定のフォーマットである文字列である場合に、適切にパースしてHTMLによるデコレーションを行います。例えば、Google Code,Trac,MoinMoinなどのWiki Engineでは次のような変換を期待します。
Input: “= Headline01 =”
Output: “<h1> Headline01 </h1>”
使用するフォーマットは、Google CodeのWiki Syntaxとします。つまり、以下のサイトが仕様書となりますので必要に応じて参照してください。 http://code.google.com/p/support/wiki/WikiSyntax
PRE TDD BOOT CAMP 札幌JAVAコミュニティ
ページ 1 / 14
Exercise 0: Interface design
はじめにインターフェイスを設計します。インターフェイスはテスト駆動で開発を行う時は特に重要です。なぜならば、単体テストは原則として「入力(インプット)に対して出力(アウトプット)が妥当であるか」を評価するため、テストしやすいインターフェイスにすることが強く求められます。例えば、以下の2つのインターフェイスを比較してください。
public abstract void convert(String text); public abstract String getFormatedText();
public abstract String convert(String text);
前者の仕様では、変換した文字列を別のメソッドで取得することを想定していると考えられます。しかし、後者の仕様では「入力に対する出力」が明らかです。つまり、単体テストを記述しやすくなるわけですが、それは使いやすいインターフェイスであるという事と同義です。テスト駆動開発の格言の1つ「APIの最初のユーザは自分である」を覚えておきましょう。
以上を踏まえた上で、以下のインターフェイスを作成してください。
package tddbootcamp.sapporo.wikiengine;
public interface WikiEngine { /** * Wikiフォーマットのテキストをhtmlに変換する. * @param text 入力テキスト * @return 変換されたhtml * / String toHtml(String text);}
もっと改善する余地はあるかもしれませんが、今はこれで我慢します。
PRE TDD BOOT CAMP 札幌JAVAコミュニティ
ページ 2 / 14
Exercise 1: First Test (20 minutes)テスト駆動開発の極意は「小さな事をコツコツと確実に」です。最初から難しい事にチャレンジするのではなく単純な事を少しづつ投入していきます。その時に、「テスト - 実装 - リファクタリング」のサイクルを回していきます。このサイクルは小さければ小さいほど、都度の実装やリファクタリングの範囲が小さくなります。また、問題が発生した時にはどこが問題なのかを特定しやすくなります。それでは最初のテストを書いてみましょう。最初に作るテストは、もちろんHello Worldです。
Test Case 入力文字列がそのまま返ることTarget Class tddbootcamp.sapporo.wikiengine.WikiEngineImplInput Hello WorldOutput Hello World
1. テストクラスの作成テスト対象のクラスは「tddbootcamp.sapporo.wikiengine.WikiEngineImpl」なのでテストクラスは「tddbootcamp.sapporo.wikiengine.WikiEngineImplTest」となります。src/test/java の下にパッケージを作成し、テストクラスを作成します。
package tddbootcamp.sapporo.wikiengine;public class WikiEngineImplTest {}
2. テストの作成テストケースからJUnitのテストコードを作成します。
@Test public void toHtml_HelloWorld() { WikiEngineImpl target = new WikiEngineImpl(); String input = "Hello World"; String expected = "Hello World"; String actual = target.toHtml(input); assertThat(actual, is(expected)); }
やや冗長かもしれませんが、 入力と出力(結果)と期待値を意識した「型」は重要です。この時点では、当然のごとくコンパイルエラーとなりますが、Java以外の言語ではテスト失敗(レッド)の1つかもしれません。
3. 実装次のステップは実装してテストを通すことです。ここでのポイントは「テストを通すために必要最低限の事しか行わない」ということです。まずはコンパイルエラーを解消するところまで進めます。EclipseではほとんどのエラーをCtrl + 1で解消できるの
PRE TDD BOOT CAMP 札幌JAVAコミュニティ
ページ 3 / 14
でフル活用してテスト対象クラスを作成しましょう。
package tddbootcamp.sapporo.wikiengine;public class WikiEngineImpl { public String toHtml(String input) { return null; }}
テスト結果はレッド(失敗)です。
4. 最低限の実装を行うそれではテストをグリーン(成功)にします。最低限の実装を行います。
public String toHtml(String input) { return "Hello World"; }
問題はあるのは兎も角、まだリファクタリングの必要はなさそうです。以上で最初のテストは完了です。
PRE TDD BOOT CAMP 札幌JAVAコミュニティ
ページ 4 / 14
Exercise 2: Second Test (15 minutes)
TDDの極意は「継続的にテンポよく」です。さっそく次のテストを書いていきましょう。1つ目のテストケースでは、固定文字列に対して入力文字列がそのまま返ることを検証しました。どんな文字列を入力してもそのような挙動が来る事は保証していません。そこで2つ目のテストパターンを入力値として用意して検証してみます。
Test Case 入力文字列がそのまま返ることTarget Class tddbootcamp.sapporo.wikiengine.WikiEngineImplInput TDD BootcampOutput TDD Bootcamp
1. テストの作成tddbootcamp.sapporo.wikiengine.WikiEngineImplTestにテストケースを追加します。テストケースは適切な粒度で分割されていると読みやすくなりますので、今回は別のテストメソッドとして実装します。
@Test public void toHtml_TDD_Bootcamp() { WikiEngineImpl target = new WikiEngineImpl(); String input = "TDD Bootcamp"; String expected = “TDD Bootcamp”; String actual = target.toHtml(input); assertThat(actual, is(expected)); }
テストを書いたら、直ぐに実行してレッドとなることを確認してください。慣れるまでは奇妙な感覚ですが、「レッド(テスト失敗)となることで安心する」感覚が芽生えるとTDDを身につけてきている証拠です。
2. 実装最初のテストがレッドにならないように気をつけながらコードを修正します。修正後のコードはこうなるでしょう。
public String toHtml(String input) { return input; }
これで2つのテストがグリーンとなりました。
3. テストコードの整理ここまでで2つの簡単なテストを作成しましたが、恐らくは2つ目のテストコードはカット&ペーストでコピーし、文字列を修正したと思います。いわゆるコピペコードですが、コピペ自体に問題があるのではありません。問題はコピペしたコードをそのままにしておくことです。コピペを行ったということは2つのコードに共通点が多い
PRE TDD BOOT CAMP 札幌JAVAコミュニティ
ページ 5 / 14
と言うことです。したがって、コードを綺麗に読みやすく修正する事が可能であり、それが必要な状況です。ただし、テストコードについては共通部分をなくせば良いというものではありません。テストコードは「入力・出力(実行)・期待値」が簡単に理解できる方が望ましいです。ここでは次のようにコードを修正しました。
public class WikiEngineImplTest { WikiEngineImpl target; @Before public void setUp() { target = new WikiEngineImpl(); } @Test public void toHtml_HelloWorld() { String input = "Hello World"; String expected = "Hello World"; String actual = target.toHtml(input); assertThat(actual, is(expected)); } @Test public void toHtml_TDD_Bootcamp() { String input = "TDD Bootcamp"; String expected = "TDD Bootcamp"; String actual = target.toHtml(input); assertThat(actual, is(expected)); }}
尚、このようにコードを整理することは「リファクタリング」と言えますが、本来、リファクタリングを行うにはテストが必要です。するとテストコードを検証するためのテストコードを書きリファクタリングして・・・という事になります。なので、リファクタリングっぽい事をしてコードを整理すると考えてください。
PRE TDD BOOT CAMP 札幌JAVAコミュニティ
ページ 6 / 14
Exercise 3: Interface Test (15 minutes)
TDDに必要なスキルは設計スキルであり、TDDを実践することで設計スキルも実装スキルも向上します。ここで検証するのはWikiEngineImplがWikiEngineインターフェイスを正しく実装しているかという事です。Javaの場合、コンパイル時に検証できるので不要なテストかもしれませんが、テストコードをどう書くか考えてみましょう。
Test Case WikiEngineImplがWikiEngineをimplementsしていることTarget Class tddbootcamp.sapporo.wikiengine.WikiEngineImplInput ?Output ?
1. テストの作成tddbootcamp.sapporo.wikiengine.WikiEngineImplTestにテストケースを追加します。
@Test public void implements_WikiEngine() { assertThat(target, is(instanceOf(WikiEngine.class))); }
テストを書いたら、直ぐに実行してレッドとなることを確認します。
2. 実装最初のテストがレッドにならないように気をつけながらコードを修正します。修正後のコードはこうなるでしょう。
package tddbootcamp.sapporo.wikiengine;public class WikiEngineImpl implements WikiEngine { @Override public String toHtml(String input) { return input; }}
これで3つのテストがグリーンとなりました。
PRE TDD BOOT CAMP 札幌JAVAコミュニティ
ページ 7 / 14
Exercise 4: Null args test (15 minutes)
インターフェイスのメソッドは正しく入力値のチェックを行うべきです。予期せぬ所でNullPointerException等が発生したならばそれはバグでしかありません。異常値に関するテストを追加してみます。
Test Case nullを入力すると例外が発生することTarget Class tddbootcamp.sapporo.wikiengine.WikiEngineImplInput nullOutput IllegalArgumentException
1. テストの作成tddbootcamp.sapporo.wikiengine.WikiEngineImplTestにテストケースを追加します。
@Test(expected = IllegalArgumentException.class) public void toHtml_null() { target.toHtml(input); }
テストを書いたら、直ぐに実行してレッドとなることを(ry。
2. 実装
package tddbootcamp.sapporo.wikiengine;public class WikiEngineImpl implements WikiEngine { @Override public String toHtml(String input) { if (input == null) throw new IllegalArgumentException("input == null"); return input; }}
これで4つのテストがグリーンとなりました。
PRE TDD BOOT CAMP 札幌JAVAコミュニティ
ページ 8 / 14
Exercise 5: Heading (15 minutes)
これまでの演習で書いたコードのほとんどはテストコードです。最初から実装コードを書いていけばもっと早く実装できるのでは?と考えるのは当然かもしれません。ですが、今まで書いてきたテストケースはこれから開発していく機能の大切な基盤となっています。テストを書く時間はかかりますが、何度も何度も繰り返して実行していくことで、最初に投資した時間は確実に回収できるでしょう。さて、そろそろ本格的にWiki Engineの機能を実装していきます。最初に実装するのはヘッドライン(h1)タグに対応するWikiフォーマットです。
Test Case = で囲まれた文字列の場合、<h1>タグで囲まれるTarget Class tddbootcamp.sapporo.wikiengine.WikiEngineImplInput = Heading =Output <h1>Heading</h1>
1. テストの作成
@Test public void toHtml_Heading() { String input = "= Heading ="; String expected = "<h1>Heading</h1>"; String actual = target.toHtml(input); assertThat(actual, is(expected)); }
テストを書いたら、直ぐに実行して(ry。
2. 実装なるべく簡単に実現できるコードを書いてみます。
package tddbootcamp.sapporo.wikiengine;public class WikiEngineImpl implements WikiEngine { @Override public String toHtml(String input) { if (input == null) throw new IllegalArgumentException("input == null"); if (input.startsWith("= ") && input.endsWith(" =")) { return "<h1>" + input.substring(2, input.length() - 2) + "</h1>"; } return input;}
これで5つのテストがグリーンとなりました。
PRE TDD BOOT CAMP 札幌JAVAコミュニティ
ページ 9 / 14
Exercise 6: Subheading (15 minutes)
ここでもっと深くテストと実装をしたい所ですが、先に広く浅くやっていくことにします。
Test Case == で囲まれた文字列の場合、<h2>タグで囲まれるTarget Class tddbootcamp.sapporo.wikiengine.WikiEngineImplInput == Heading2 ==Output <h2>Heading2</h2>
1. テストの作成
@Test public void toHtml_Heading2() { String input = "== Heading2 =="; String expected = "<h2>Heading2</h2>"; String actual = target.toHtml(input); assertThat(actual, is(expected)); }
テストを書いたら、直ぐに(ry。
2. 実装なるべく簡単に実現できるコードを書いてみます。
package tddbootcamp.sapporo.wikiengine;public class WikiEngineImpl implements WikiEngine { @Override public String toHtml(String input) { if (input == null) throw new IllegalArgumentException(“input == null”); if (input.startsWith("= ") && input.endsWith(" =")) { return "<h1>" + input.substring(2, input.length() - 2) + "</h1>"; } else if (input.startsWith("== ") && input.endsWith(" ==")) { return "<h2>" + input.substring(3, input.length() - 3) + "</h2>"; } return input; }}
これで6つのテストがグリーンとなりました。if文だらけでちょっとコードがカオスになってきてますが、必要最低限のテストは通っているのでアプリケーションの価値は低くありません。
PRE TDD BOOT CAMP 札幌JAVAコミュニティ
ページ 10 / 14
Exercise 7: Refactering (20 minutes)
次に実装していきたいフィーチャはh3, h4, h5, h6への対応です。テストを書いて実装する道筋は見えきますが、コピペをする事も予想できます。コピペで進んでいっても良いのですが、その事を予測し、ここで一旦リファクタリングを行いましょう。リファクタリングはコピペが行われた後にすぐ行うのが効果的です。リファクタリングとは「外部から見たときの振る舞いを保ちつつ、理解や修正が簡単になるように、ソフトウェアの内部構造を変化させること」です。つまり、入力に対する出力は変えずに、オブジェクト指向プログラミングの手法や可読性を高める工夫をすることで、アプリケーションの可読性・拡張性・再利用性などを高めていく事になります。
1. リファクタリングの検討リファクタリングを行うために、コードの共通部分と相違部分をチェックします。
if (input.startsWith("= ") && input.endsWith(" =")) { return "<h1>" + input.substring(2, input.length() - 2) + "</h1>"; } else if (input.startsWith("== ") && input.endsWith(" ==")) { return "<h2>" + input.substring(3, input.length() - 3) + "</h2>"; }
すると、比較する文字列の違いは=の数であり、その数に関連した文字列がsubstringで切り出されていることが解ります。ここは正規表現を使えば上手く処理できそうです。
2. リファクタリングpublic class WikiEngineImpl implements WikiEngine { static final Pattern HEADER_PATTERN = Pattern.compile("^(=+) .* (=+)$"); @Override public String toHtml(String input) { if (input == null) throw new IllegalArgumentException("input == null"); Matcher m = HEADER_PATTERN.matcher(input); if (m.find()) { String start = m.group(1); String end = m.group(2); if (start.length() == end.length()) { int level = start.length(); String body = input.substring(level + 1, input.length() - level - 1); return "<h" + level + ">" + body + "</h" + level + ">"; } } return input; }}
実装したならばテストを実行してグリーンであることを確認します。正規表現を用いることでやや可読性は落ちましたが、レベルが増えても対応できるコードとなりました。
PRE TDD BOOT CAMP 札幌JAVAコミュニティ
ページ 11 / 14
3. テストの追加h3, h4, h5, h6のテストを順番に追加しましょう。
@Test public void toHtml_Level3() { String input = "=== Level3 ==="; String expected = "<h3>Level3</h3>"; String actual = target.toHtml(input); assertThat(actual, is(expected)); }
テストを書いたら、直ぐに(ry。
@Test public void toHtml_Level4() { String input = "==== Level4 ===="; String expected = "<h4>Level4</h4>"; String actual = target.toHtml(input); assertThat(actual, is(expected)); }
テストを書いたら(ry。
@Test public void toHtml_Level5() { String input = "===== Level5 ====="; String expected = "<h5>Level5</h5>"; String actual = target.toHtml(input); assertThat(actual, is(expected)); }
テストを(ry。
@Test public void toHtml_Level6() { String input = "====== Level6 ======"; String expected = "<h6>Level6</h6>"; String actual = target.toHtml(input); assertThat(actual, is(expected)); }
テ(ry。
これで10個のテストがグリーンになりました。尚、リファクタリングで書き換えたコードは一例ですので、各自で自由な発想で実装してみてください。一番重要なことは外から見た振る舞いが変わらないこと、つまりテストが通ることです。
PRE TDD BOOT CAMP 札幌JAVAコミュニティ
ページ 12 / 14
Exercise 8: Levl7 (10 minutes)
Google Codeの WikiSyntaxの仕様では、対応するヘッダタグはLevel6までのようです。今のままでは幾らでもレベルを深くすることが出来てしまいまうでしょう。テストを作り、制御を加えましょう。
Test Case ======= で囲まれた文字列の場合、そのままTarget Class tddbootcamp.sapporo.wikiengine.WikiEngineImplInput ======= Level7 =======Output ======= Level7 =======
1. テストの作成 @Test public void toHtml_Level7_unsupport() { String input = "======= Level7 ======="; String expected = "======= Level7 ======="; String actual = target.toHtml(input); assertThat(actual, is(expected)); }
予想通りにレッドとなるでしょう。
2. 実装最大レベルを設定します。
public class WikiEngineImpl implements WikiEngine { static final Pattern HEADER_PATTERN = Pattern.compile("^(=+) .* (=+)$"); @Override public String toHtml(String input) { if (input == null) throw new IllegalArgumentException("input == null"); Matcher m = HEADER_PATTERN.matcher(input); if (m.find()) { String start = m.group(1); String end = m.group(2); if (start.length() < 7 && start.length() == end.length()) { int level = start.length(); String body = input.substring(level + 1, input.length() - level - 1); return "<h" + level + ">" + body + "</h" + level + ">"; } } return input; }}
これでテストは通りますが、マジックナンバーがありますし、やや読みにくくなってきています。リファクタリングしましょう。
PRE TDD BOOT CAMP 札幌JAVAコミュニティ
ページ 13 / 14
3. リファクタリングリファクタリングを行いマジックナンバーを定数にしました。public class WikiEngineImpl implements WikiEngine { static final Pattern HEADER_PATTERN = Pattern.compile("^(=+) .* (=+)$"); static final int HEADER_MAX_LEVEL = 6; @Override public String toHtml(String input) { if (input == null) throw new IllegalArgumentException("input == null"); Matcher m = HEADER_PATTERN.matcher(input); if (m.find()) { String start = m.group(1); String end = m.group(2); int level = start.length(); if (level <= HEADER_MAX_LEVEL && level == end.length()) { String body = input.substring(level + 1, input.length() - level - 1); return "<h" + level + ">" + body + "</h" + level + ">"; } } return input; }}
これで11のテストがグリーンとなりました。随分とテスト駆動開発っぽくなってきましたね。このようにテストを先に行うことで、次に何を実装していくかをはっきりとさせる効果があります。また、開発していく中で実装を修正してもテストが通れば安心である為、思い切ったリファクタリングもできるようになります。
More Exercise:これ以降は、好きなWiki Syntaxを選択し、各自でテスト駆動開発していきましょう。また、現在は複数行の入力には対応していませんので、複数行入力に対する対応も必要になってきます。
この資料について
このドキュメントに関して、個人またはコミュニティレベルで使用する範囲ではご自由に使っていただいて構いません。地方の勉強会や社内勉強会などでご自由に使用してください。ただし、再配布に関しては責任が持てませんのでご遠慮願います。
Author: Shuji Watanabe (札幌Javaコミュニティ、[email protected])
PRE TDD BOOT CAMP 札幌JAVAコミュニティ
ページ 14 / 14