はじめに
先日、TDDBCに参加してきました。
このイベントで色々と学ぶことがあったので書きます。
目次
今回の題材
今回のお題は「自動販売機」でした。
プログラミングのお題: 自動販売機 (設計進化重視バージョン) · GitHub
ペアプロ時の工夫
TDDBCではTDDだけでなく、ペアプロも体験しました。 なお、今回は私(テストエンジニア)が常にナビゲータの立場、相方が常にドライバーの立場でペアプロを行いました。 そんな中、今回工夫した点をこのブログで共有します。
工夫その1:忠実に実装する
まずは、お題1を考えた時の工夫点です。
お題1. ボタンを押すとコーラが出る
ボタンを押すとコーラが出ます。
このお題に対し、コードは以下のように書きました。
public String buy() { return "コーラを購入しました"; }
しかし、これでは「ボタンを押すとコーラが出る」ではなく、「購入しようとするとコーラを購入できる」となり、お題の意図以上に業務全体を考えた実装になってしまっています。 なので、「ボタンを押すとコーラが出る」というお題に対して忠実に実装すべく、以下のように変更しました。*1
public String pushButton() { return "コーラ"; }
工夫その2:お題を分割する
お題1を終わらせた後、お題2を着手しました。
お題2. お金を払う
100円コインを投入してからボタンを押すとコーラが出ます。
100円コイン以外は投入できません。
このお題をそのまま行うのではなく、TODOの分割に着手しました。
TODOに落とし込むための会話ログ
以下は、TODOに落とし込むために行った会話です。
- 自分「これって、どんなテストがありますかね?」
- 相手「まず、100円コインを投入するとボタンが有効になるか(★1)、ですかね」*2
- 自分「なるほど。100円コインを入れた場合の話ですね。他にはどんなテストがありますか?」
- 相手「えっと、100円以外のコインを投入しても、ボタンが有効にならない場合ですかね。」
- 自分「そうですね。ただ、『100円以外』とは具体的にはどんな値が考えられますか?」
- 相手「例えば、10円コインですかね。」
自分「ふむふむ。ちなみにこれらって、『ボタンが有効になる条件』をテストしていますね」*3
自分「それでは、他にはどんなテストが考えられますか?」
- 相手「ボタンを押した結果のテスト(★2)をしたいです」
- 自分「ボタンを押した結果のテストって、具体的にはどんなことを考えてますか?」
- 相手「ボタンが有効な場合に限り、ボタンを押すとコーラが出るようなテストですかね」
- 自分「なるほど。それって、具体的なテストを考えると、どういう風になると考えてますか?」
- 相手「100円コインを投入した後にボタンを押すとコーラが出るテストですかね」
- 自分「ほうほう。そうですね。それ以外だとどうですか?」
- 相手「ボタンが無効な場合、ボタンを押すと何も出ないことをテストしたいですね。」
- 自分「それって、具体的にはどんなテストになりますか?」
- 相手「10円コインを投入した後にボタンを押すと、何も出ない、とかですかね。」
- 自分「なるほど。」
TODOを階層構造化する
ここまでを踏まえて、私は以下のように階層構造化して整理しました。
ボタンが有効になる条件をテストする
- 100円コインを投入するとボタンが有効になる(★1)
- 100円以外のコインを投入してもボタンが有効にならない
- 10円コインを投入してもボタンが有効にならない
ボタンを押した結果のテスト(★2)
- ボタンが有効な場合に限り、ボタンを押すとコーラが出る
- 100円コインを投入した後にボタンを押すとコーラを出る
- ボタンが無効な場合、ボタンを押すと何も出ない
- 10円コインを投入した後にボタンを押すと何も出ない
階層構造化した文章のうち、青字は相方が思いついた部分、緑字は私との会話で加えていった部分です。
テスト設計の知識と紐付ける
今回行った会話は非常に興味深いです。
前半部分は(★1)のように、具体的な値を思いついて、そこからテストの条件を考えている、つまりボトムアップ型の考え方を行っています。
一方、後半部分は(★2)のように、どんなテストをすべきか考えて、そこから具体的な値を考えている、つまりトップダウン型の考え方を行っています。
実は、このボトムアップ型、トップダウン型という表現は、先日あったテスト設計コンテストU30クラスチュートリアルの資料及び発表でも話しています。
http://aster.or.jp/business/contest/doc/2019_U-30_V1.0.0%20.pdf#page=13
以下のツイートは、テスト設計コンテストU30クラスチュートリアルの発表聴講時のツイートですが、この発言は上記の会話のことを指しています。
ちなみに先日のTDDブートキャンプでは、まさにボトムアップ的なアプローチとトップダウン的なアプローチの両方が、同じ人から出てきて面白かった#テスコン
— ブロッコリー (@nihonbuson) 2018年10月2日
工夫その3:Greenのままリファクタリングを行う
最後はお題3での工夫です。
お題3. ウーロン茶追加
押したボタンに応じてコーラかウーロン茶が出ます。
他の飲み物も追加してみましょう。
準備:お題2までのコード
お題2までで、こんなコードが出来上がっていました。(一部抜粋)
private boolean canBuy = false; public String pushButton() { if (canBuy){ return "コーラ"; } return ""; }
一方、テストコードはこんな感じで書いていました。(一部抜粋)
@Test public void 100円コインを投入した後にボタンを押すとコーラを出る() { VendingMachine vendingMachine = new VendingMachine() vendingMachine.insertCoin(100); String result = vendingMachine.pushButton(); assertThat(result, is("コーラ")); } @Test public void 10円コインを投入した後にボタンを押しても何も出ない() { VendingMachine vendingMachine = new VendingMachine() vendingMachine.insertCoin(10); String result = vendingMachine.pushButton(); assertThat(result, is("")); }
お題3の方針
お題3を考えると、以下のようなテストコードを考えたくなります。
private static final int COLA = 1; @Test public void 100円コインを投入した後にコーラのボタンを押すとコーラが出る() { VendingMachine vendingMachine = new VendingMachine() vendingMachine.insertCoin(100); String result = vendingMachine.pushButton(COLA); assertThat(result, is("コーラ")); }
上記のテストコードを実現するには、お題2で"pushButton()"というメソッドだったのを、"pushButton(id)"というメソッドに変更することになります。
しかし、そのままメソッドの引数を変更すると、今度は今まで通っていた、"pushButton()"メソッドを用いたテストコードが通らなくなります。
新たなテストコードをGreenにするために、既存のテストコードを修正することになるため、新規テストコードのRed→Greenなのか、既存テストコードのRefactoringなのかが区別つかなくなります。*4
どうすれば良いでしょう…?
その時、教わった方法に納得しました。
既存コードをKeepしたまま実装する方法
手順1:別メソッドを作る
まず、別メソッド名で作ります。
private static final int COLA = 1; @Test public void 100円コインを投入した後にコーラのボタンを押すとコーラが出る() { VendingMachine vendingMachine = new VendingMachine() vendingMachine.insertCoin(100); String result = vendingMachine.pushButtonWithProductId(COLA); assertThat(result, is("コーラ")); }
private boolean canBuy = false; public String pushButton() { if (canBuy){ return "コーラ"; } return ""; } public String pushButtonWithProductId(int id) { if (canBuy){ return "コーラ"; } return ""; }
上記のように、"pushButton()"とは別のメソッド"pushButtonWithProductId(id)"を作り、新規のテストコードは"pushButtonWithProductId(id)"を用います。
その際、"pushButtonWithProductId(id)"のメソッドの中身は、"pushButton()"のメソッドの中身をそのままコピペします。
これで、既存テストコードを変更することなく、Red→Greenにすることができました。
手順2:元々のメソッドを変更する
次に、"pushButton()"の中身を以下のように変えます。
private static final int COLA = 1; private boolean canBuy = false; public String pushButton() { return pushButtonWithProductId(COLA); } public String pushButtonWithProductId(int id) { if (canBuy){ return "コーラ"; } return ""; }
元々、メソッドの中身は"pushButton()"と"pushButtonWithProductId(id)"で全く同じでした。
なので、メソッドを呼び出しても同じ結果なはずです。*5
手順3:呼び出すメソッドを変更する
そして最後に、既存のテストコードで呼び出すメソッドを変更します。
すべてメソッドを"pushButtonWithProductId(id)"に変更したら、"pushButton()"が必要なくなるので、削除します。
private static final int COLA = 1; @Test public void 100円コインを投入した後にボタンを押すとコーラを出る() { VendingMachine vendingMachine = new VendingMachine() vendingMachine.insertCoin(100); String result = vendingMachine.pushButton(COLA); assertThat(result, is("コーラ")); } @Test public void 10円コインを投入した後にボタンを押しても何も出ない() { VendingMachine vendingMachine = new VendingMachine() vendingMachine.insertCoin(10); String result = vendingMachine.pushButton(COLA); assertThat(result, is("")); }
おわりに:ブートキャンプから学べたこと
今回は、テストコードもプロダクトコードも少ししか書いていませんでした。
しかし、ここで学んだことは、実務でも使える実践的な方法だったと思います。*6
それをブートキャンプの中で学べたのは非常に有意義でした。