こんにちは。WEBインテグレーション部で受託開発やらせていただいておりますアサヒです。
モンハンが発売されましたね、ブログ書かずに帰りたい衝動に駆られています。
平日の夜にいろんな人とモンハンやりたいなぁと思う今日この頃です。
さて、今回は「テスト駆動開発」の演習をやってみました。
(URL: https://codeiq.jp/magazine/2013/11/1475)
新卒で集まって「テスト駆動開発」の読書会をしていたのですが、
第一部を読み終わって、第二部に進むよりもまずは簡単な問題で実践してみたいという欲が強まりました。
そしたら、ネットに落ちているではありませんか演習問題。
今回はその時の問題の進め方と出来上がったコード、やってみて気づいた問題点などを書いていこうと思います。
その前に
テスト駆動開発は実装をテストするのではなく、テストが通るような実装を書いて綺麗な設計を目指す開発手法です。
それを、「テスト駆動開発」の書籍に書いてある開発フローを基にして演習を解いてみました。
開発フローは以下の通りです。
- TODOリストからやることを選ぶ
- テストを書く
- コンパイラを通す
- テストを走らせ、失敗を確認する
- テストを通す
- 重複を排除する
最初に何のテストを書くのかをTODOリストから決めていきます。
TODOリストとは、テストするべきことやリファクタリングするべき対象を書いておくシートです。
いつ書くのかは決まっておらず、開発フローの中で気づいたら書いていくメモのようなものです。
TODOリストはテストするべき対象だけを書くものではないということに気をつけましょう。
TODOリストに書いてあることを全て終えると開発終了となります。
演習問題概要
「現在時刻」に応じて、挨拶の内容を下記のようにそれぞれ返す機能を作成したい。
(ただし、タイムゾーンはAsia/Tokyoのままとする)
- 朝(05:00:00以上 12:00:00未満)の場合、「おはようございます」と返す
- 昼(12:00:00以上 18:00:00未満)の場合、「こんにちは」と返す
- 夜(18:00:00以上 05:00:00未満)の場合、「こんばんは」と返す
開発
TODOリストを項目ごとにセクションに分けました。
片付けていった順にやっていったことを説明します。
「おはようございます」と返す
いきなり条件から書き出すのではなく、シンプルに「おはようございます」を返すようにしました。
1 2 3 4 5 6 7 |
public class GreetTest { @Test public void おはようございますを返すgreet() { Greeter greeter = new Greeter(); assertEquals("おはようございます", greeter.greet()); } } |
1 2 3 4 5 |
abstract class Greeter { public String greet () { return "おはようございます"; } } |
文字列を返す、で良かったのでは?
「おはようございます」以外にもパターンがあるので、もっとシンプルに文字列を返せばOKとしました。
1 2 3 4 5 |
@Test public void 文字列を返すgreet() { Greeter greeter = new Greeter(); assertEquals(String.class, greeter.greet().getClass()); } |
文字列「05:00:00」の場合、「おはようございます」と返す
時間の指定をいきなりDateにするのは、考えることが増えてめんどくさいので一旦文字列を条件にしてみました。
あまりにもひどいコードですが、テストは通ったし重複もないのでこのまま進みます。
TODOリストには「Dateでの比較に変える」「timestampへのアクセスの仕方を変える」を追加しておきます。
1 2 3 4 5 6 |
@Test public void おはようございますを返すgreet() { Greeter greeter = new Greeter(); greeter.timestamp = "05:00:00"; assertEquals("おはようございます", greeter.greet()); } |
1 2 3 4 5 6 |
public String timestamp = ""; public String greet () { if(timestamp.equals("05:00:00")) return "おはようございます"; return ""; } |
文字列「12:00:00」の場合、「こんにちは」と返す
これは、先ほどの「return “”;」が返す文字列を「”こんにちは”」にしてあげました。
1 2 3 4 5 6 |
@Test public void こんにちはを返すgreet() { Greeter greeter = new Greeter(); greeter.timestamp = "12:00:00"; assertEquals("こんにちは", greeter.greet()); } |
朝(05:00:00以上 12:00:00未満)の場合、「おはようございます」と返す
文字列だとしても、ちゃんと比較して出すようにしてあげます。
文字列をInt型に変えて、hourで比較して対応してみました。
Dateに変えなくてもhourで比較すればいいんじゃないかと思ったので、TODOリストの「Dateで比較」を「timestampをDateに変更」に変えておきます。
1 2 3 4 5 6 7 |
public String greet () { String[] timestamps = timestamp.split(":"); Integer hour = Integer.valueOf(timestamps[0]); if(hour >= 5 && hour < 12) return "おはようございます"; return "こんにちは"; } |
timestampへのアクセスの仕方を変更
直接アクセスしているのが気に食わなかったので、直します。
このTODO項目は後の方に出てきたものですが、簡単そうだったので先にやりました。
やることの順番を強制されないのもTODOリストのいいところな気がします。
あと、ここでtimestampを指定しないと常に00:00:00なのが気持ち悪くなってきたので、
TODOリストに「timestampをセットしなかった場合」を追加しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 |
@Test public void おはようございますを返すgreet() { Greeter greeter = new Greeter(); greeter.setTimestamp("05:00:00"); assertEquals("おはようございます", greeter.greet()); } @Test public void こんにちはを返すgreet() { Greeter greeter = new Greeter(); greeter.setTimestamp("12:00:00"); assertEquals("こんにちは", greeter.greet()); } |
1 2 3 4 |
private String timestamp = "00:00:00"; public void setTimestamp(String timestamp) { this.timestamp = timestamp; } |
timestampをDateに変更
やっとここでDateを使います。
最初に使うのと、ここでDateに変更するの、どっちが簡単かといえば後者な気がしますね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); @Test public void おはようございますを返すgreet() { Greeter greeter = new Greeter(); greeter.setDate(sdf.parse("05:00:00")); assertEquals("おはようございます", greeter.greet()); } @Test public void こんにちはを返すgreet() { Greeter greeter = new Greeter(); greeter.setDate(sdf.parse("12:00:00")); assertEquals("こんにちは", greeter.greet()); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
private Date date; Greeter() { this.date = new Date(); } public void setDate(Date date) { this.date = date; } public String greet () { int hour = date.getHour(); if(hour >= 5 && hour < 12) return "おはようございます"; return "こんにちは"; } |
ここで、「setDateせずに使おうと思ったとき、インスタンス化されてからdateの時間進まなくね・・・?」ということに気づきます。
これもTODOリストに加えておきましょう。(気分的に後回しにしたかった)
夜(18:00:00以上 05:00:00未満)の場合、「こんばんは」と返す
ようやくこんばんはのテストを行います。
他のTODOの方が気になったので後回しにしてましたが、結果的に修正の量が減って開発が楽でした。
1 2 3 4 5 6 |
@Test public void こんばんはを返すgreet() { Greeter greeter = new Greeter(); greeter.setDate(sdf.parse("18:00:00")); assertEquals("こんばんは", greeter.greet()); } |
1 2 3 4 5 6 7 8 |
public String greet () { int hour = date.getHour(); if(hour >= 5 && hour < 12) return "おはようございます"; if(hour >= 12 && hour < 18) return "こんにちは"; return "こんばんは"; } |
greet宣言時の時刻を確認する
最後にインスタンス化されてからdateが進まない問題を対処します。
setDateしたdateは進めたくないので、getDateでdateがない場合の処理を追加しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public Greeter() {} public Date getDate() { if(date == null) return new Date(); return date; } public String greet () { int hour = getDate.getHour(); if(hour >= 5 && hour < 12) return "おはようございます"; if(hour >= 12 && hour < 18) return "こんにちは"; return "こんばんは"; } |
出来上がったコード
GreeterTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
public class GreetTest { SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); @Test public void 文字列を返すgreet() { Greeter greeter = new Greeter(); assertEquals(String.class, greeter.greet().getClass()); } @Test public void おはようございますを返すgreet() { Greeter greeter = new Greeter(); greeter.setDate(sdf.parse("05:00:00")); assertEquals("おはようございます", greeter.greet()); } @Test public void こんにちはを返すgreet() { Greeter greeter = new Greeter(); greeter.setDate(sdf.parse("12:00:00")); assertEquals("こんにちは", greeter.greet()); } @Test public void こんばんはを返すgreet() { Greeter greeter = new Greeter(); greeter.setDate(sdf.parse("18:00:00")); assertEquals("こんばんは", greeter.greet()); } } |
Greeter.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Greeter { private Date date; public Greeter() {} public void setDate(Date date) { this.date = date; } public Date getDate() { if(date == null) return new Date(); return date; } public String greet () { int hour = getDate.getHour(); if(hour >= 5 && hour < 12) return "おはようございます"; if(hour >= 12 && hour < 18) return "こんにちは"; return "こんばんは"; } } |
問題点
仕様見落とし
演習問題には、
タイムゾーンはAsia/Tokyoのままとする
と書いてありますね?
実装にはタイムゾーンの設定をしている箇所がありません・・・。(やってしまった)
TODOリストには、仕様を漏れなく書いておかないといけませんね・・・。
正常値・境界値・例外値
このテストコードでは、正常値のみでテストされています。
17:59:59のような境界値や、DateにNULLが入ってきた時の例外値でのテストがされていないのです。
気づかなかったわけではないのですが、TODOリストにやるべきこととして書いていなかったので完全に見落としてました。
TODOリストには「やるべきこと」を書くのですが、「やるべき」というのは経験しないとわからないのかもしれないです。
それが今回の演習でよかったなぁと思いました(小並感)
(・・・すみません、次はちゃんとチェックしますっ!!!!)
TODOリストには「やるべきこと」だけじゃなくて、「こうしたらどうなるんだろう?」という疑問や「こうしたい」という願望もあっていいのかもしれないですね。
コードに対する不安や不満を解決することも「やるべきこと」であるという気づきがありました。
開発フローすっ飛ばし
テスト駆動開発の大まかなフローは、
- テストを書く
- コンパイラを通す
- テストを走らせ、失敗を確認する
- テストを通す
- 重複を排除する
という流れになっています。
実践でこれを行うと、以下のような問題が出てきました。
- TODOリストを最初にまとめてしまう
- 明確な実装を頭に思い描いてから始めてしまうため
- 実装から入ってしまう
- 思い描いてるTODOに自信がないので、実装で確かめたくなるため
- 失敗の確認や一旦テストを通すフローを飛ばしてしまう
- この作業フローの目的が明確になっていないため
結果、2~5をまとめて、テスト書いて実装してテスト通すだけになってしまうケースがありました。
対策としては、「TODOリストをより簡単にする」のがいいのかなと思いました。
理由は以下の通りです。
- 手が止まることが少ないので、最初の段階で大まかな実装を考えてしまう心配がない
- 仕様をめちゃくちゃ簡単にすると、TODOリストに自信が持てる(今回でいえば「文字列を返す」だけとか)
実装から入ることが少なくなるから、何が終わっていて何が終わってないのか明確になる - どういう処理を追加するのか予想できるので、それを確かめるための「失敗の確認」や
何を返すか明確にするための「一旦テストを通す」作業の大切さを再認識できる
最後に
イッチーくん、ゆりちゃん、読書会に付き合ってくれてありがとう。
実は読書会は僕含め3人でやっていたのですが、今回の演習はそれぞれ考えられているところや発見があって楽しかったです。
よく大勢でその場でコードを書いて見せ合ったりする会がありますが、少人数では宿題で書いてきたコードを見せあう方が合っているのかもしれません。
その場でコードを書くと、時間に焦って考え尽くされたコードが書けませんし、大勢で宿題にすると書いてこない人が続出します。
少人数だと自分が発表することがわかっているので基本的には書いてくるし、考える時間もあるのでそれぞれのアイデアなどが見れて面白かったです。
またテスト駆動開発の勉強をしていって、実践で役に立てることができるよう精進していきます!
最後まで読んでいただき、ありがとうございました!
エンジニア3年目 プランニング部に所属しています。 海外向けプロダクトでの開発を1年、マーケティングツールのデータ整備やそれを用いた業務改善ツールの開発を1年やってきています。 ラーメンのことを考えすぎて、脳がラーメンになりました。