テストエンジニアがTDDブートキャンプに参加してきました #TDDBC

はじめに

先日、TDDBCに参加してきました。

tddbc.connpass.com

このイベントで色々と学ぶことがあったので書きます。

目次

今回の題材

今回のお題は「自動販売機」でした。

プログラミングのお題: 自動販売機 (設計進化重視バージョン) · 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クラスチュートリアルの発表聴講時のツイートですが、この発言は上記の会話のことを指しています。

工夫その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

それをブートキャンプの中で学べたのは非常に有意義でした。

*1:変更は、Greenの状態の時にRefactoringとして行いました。

*2:お題1で、業務全体ではなく実装に忠実になることを考えた結果、分割されたこのテストを思いつくのが素晴らしいですし、純粋にスゴイと思いました!

*3:本当は、ここで「コインを入れない場合」を思いついていますが、ここでは敢えて言いませんでした

*4:今回の例だと、同時に直せば良いと思うかもしれないですが、修正箇所が数十箇所あったらどうでしょうか?

*5:もちろん、このRefactoring中にもテストは常に回してGreenであることを確認します

*6:これがKataなのか…!