TDDBC札幌2019に参加してきました

2019年6月15日に開催された「TDDBC札幌2019」に参加してきました!

f:id:ryu19j:20190630001026j:plain
TDDBC札幌2019会場入り口

agilesapporo.doorkeeper.jp

午前中は和田卓人さんの基調講演&ライブコーディング、午後はペアプロでTDDとレビューを行いました。

当日学んだことをあとで思い出せるように、感想とメモ書きを書いておこうと思います。

基調講演&ライブコーディング

speakerdeck.com

上記のスライドを用いて基調講演が始まりました。
FizzBuzzを題材にライブコーディングが行われたのですが、これが本当にすごかったです。
スライドのタイトル通り「見てわかるテスト駆動開発」でした。
何を考えながらコードを書いているのかがとてもわかりやすく、本当に素晴らしかったです。

ペアプロでTDD&レビュー

■お題 gist.github.com

面識のない人とのペアプロは今までやったことがなかったので、うまくできるか不安でしたが、ペアの方がフォローしてくださったので、とても楽しく行うことができました! 実際にペアで考えながら手を動かしてRed→Green→Refactoringを行うことで、楽しくTDDを学ぶことができたと思います!

今回は前半と後半でレビューの時間があったのですが、知らないうちに前半のレビューの対象となっていて、気づいたらマイクを持たされていました。
(てっきり「レビュー受けてみたい人いますかー?」とか言われて挙手するスタイルかなーと勝手に思っていたのですが…)

レビューではたくさんのマサカリ ご指摘をいただきました。 ほとんどの指摘が「仰る通り」としか言えない正しい指摘でした。
当たり前のことであっても、指摘されて初めて気づけることも多いので、レビューはやっぱり大事だなと改めて実感しました。

いきなりマイクを持たされて少し緊張しましたが、割と落ち着いてコードの説明をすることができたと思います。
コードに対して指摘されたときや、意図の説明を求められたときも意外と冷静に回答できていたような気がします。 (回答の8割くらいは「仰る通りです」だったような気もしますが…!)

とても貴重な経験をさせてもらえてラッキーでした!

[質問・指摘内容(うろ覚え)]

* 閉区間の下端点と上端点を持つことのテストを書いた理由
* equalsがJava言語仕様に沿っていない
  * 仰る通りです
* 例外のテストに`@Test(expected = ~Exception.class)`を使用しなかったのか
  * 存在は知っていたが、自信がなかったので使用しなかった
* チェック処理のメソッド名が`check~`
  * 改善例として`require~`として、必須項目が入力されていない場合は例外をスロー
* ゲッターとフィールド名にズレがある
  * 仰る通りです
* テストケース不足
  * 仰る通りです

ペアプロ前半の時点でのコードなので、まだ中途半端な状態なのは仕方ないかなとは思うものの、初歩的な指摘が多くなった印象でした。
TDDのRed→Greenを素早く行いすぎて、Refactoringが十分ではなかったのかもしれないと思いました。
どんどん次に進みたいという気持ちが強かったのも原因かもしれません。

equalsがJava言語仕様に沿っていない部分については、少し考えがあってあえてそういった実装で進めていました。
理由は、ペアの方は第一言語Java以外の言語を選択されていて、普段Javaを書いている感じではなさそうに感じたので、最初のうちはJava言語仕様に関わる部分に触れたくないなと思ったからでした。
というのも、ペアプロの最初から「これ、Javaの言語仕様だからこう書くべきです」みたいな指摘をしてしまうと、積極的に意見を言えずに萎縮してしまう人もいるかもしれない、と思ったからです。
自分はペアと出来るだけ対等な関係で進めたかったので、まずはequalsをあえて簡単な実装にして進め、ペアプロに慣れてきたタイミングでRefactoringする流れで行こうと思いました。

ただ、今になってみると少し考えすぎだったかもなと思うし、「動作する綺麗なコード」を書きたいのだから、最初からそうあるべきだとわかっていることは指摘するべきだったかなと思います。 言葉を柔らかくして指摘するだけで十分だったかもしれません。

■書いたコード github.com

全体的な感想&まとめ

このイベントに参加する前に予習しておこうと思い、「テスト駆動開発」で少し勉強してからイベントに臨みました。
(第1章〜第16章までは軽く目を通しました) そのおかげで、基調講演&ライブコーディングの内容をスムーズに理解できて、新しく知る部分について集中して聴くことができました。

特にライブコーディングでの学びが多かったなと思いました。
リアルタイムでコードを書きながら、それについて的確な説明があることで、こんなに説得力と納得感が生まれることにとても驚きました。

ペアプロはペアの方がうまくフォローしてくださったので、楽しくコードを書くことができました!
参加者全員の前でレビューを受けて、たくさんのマサカリご指摘をもらう貴重な体験ができて本当によかったです。

今回は懇親会にも参加しました。
今までこういったイベントの懇親会には参加していなかったのですが、今回は試しに一度参加してみようかなという気分になったので参加してみました。
(面識のない人しかいない場に乗り込むのは苦手なので…)

参加者に知り合いがいなくてすごく不安でしたが、色々な人たちと話をすることができてすごく楽しく有意義な時間でした!
技術者同士で技術について話したり聞いたりするのはやっぱり楽しい! 話しかけてくださった皆さま、ありがとうございました!
今後はイベントの懇親会にも参加していこうと思います。

以上、感想でした。

(おまけ)当日のメモ書き

■メモ書き

テストを書くことで品質管理する、は副作用
分割統治、各個撃破
動作する綺麗なコード
→動作する、綺麗にする

コードを書いていると集中モードに入って、何をすべきかを忘れてしまう
→TODOリストを改訂して進んでいく
→最初は弱い奴から倒していく

Red→Green→Refactoring
リファクタリングが大事!
リファクタリングの沼(いくらでもできてしまう)
→時間制限を設ける
→数で決める(2つの重複した処理を1つにする)
FizzBuzzライブコーディング
・TODOリストを作る
→問題を分割して各個撃破する
→テスト容易性の設計
→大事なのはロジック(3のとき、5のとき、15のとき)
  →大事な部分をテストしやすいようにする
  →プリントする、は難しい。変換する、なら簡単(単純なインプットとアウトプット)
→日本語表記の揺れを整える

■設計する
==== TODOリスト
-[ ] 数を文字列に変換する
-[ ] 3の倍数の時は数の代わりに「Fizz」に変換する
-[ ] 5の倍数の時は数の代わりに「Buzz」に変換する
-[ ] 3と5の両方の時は代わりに「FizzBuzz」に変換する

-[ ] 1からxxxまで

-----------

-[ ] 1から100までの数(後回し、怠惰)
-[ ] プリントする
  →標準出力に出るかどうかをテストするのは難しいし、頑張った割に価値がない(ビューのテストみたい)

■テストを書く
とりあえずfail()
→JUnitがちゃんと動くことがわかる
  →Junitがインストールされていない、IDEが認識していない、という状況を防ぐ

日本語テストメソッドおすすめ(void 数を文字列に変換する())
→国際的なチームでなければ
→テストコードはドキュメントだから、わかりやすくあるべき
  →test01, test02とかだとなんのテストかわからない

テストメソッドの内容(フェーズ)
→準備、実行、検証
  →あえて下から書いていく
    →検証を先に書くことで、ゴール(単一の)を明確にするため
    →たくさんassertがあると何を確認したいかわからなくなる問題

assertの引数の順番
→言語によってexpected, actualの順番が違うので注意

==== TODOリストを改定
- [ ] 数を文字列に変換する
    - [ ] 1を渡すと文字列"1"に変換する
- [ ] 3の倍数の時は数の代わりに「Fizz」に変換する
- [ ] 5の倍数の時は数の代わりに「Buzz」に変換する
- [ ] 3と5の両方の時は代わりに「FizzBuzz」に変換する

- [ ] 1からxxxまで

どんな風にするか考える
→どんなクラス名、メソッド名
→作る前に使う(クラスを作る前に、テストコードに書く)
  →使いやすいかどうかを考えるフェーズ
// 準備
FizzBuzz fizzBuzz = new FizzBuzz();

// 実行
String actual = fizzBuzz.stringify(1);

// 検証
assertEquals("1", actual);
↑テストは失敗するけど、エラー内容が変わる
↑最初は簡単な物を選ぶ(最初は大変だから)
public String stringify(int i) {
  return "1";
}
// 準備
FizzBuzz fizzBuzz = new FizzBuzz();

// 実行
String actual = fizzBuzz.stringify(1);

// 検証
assertEquals("1", actual);
↑グリーンになる!(茶番のようで茶番じゃない)
  ↑テストコードがバグっているのでは?という心配
  ↑プロダクトコードを使ってテストコードが正しいかを確かめる
  ↑(欠陥挿入、ミューテーションテスティング)
  ↑現実のプロダクトコードでは現実的ではない
  ↑だからテストコードを書くタイミングで行う(だから return "1";)
【リファクタリング】
必ずテストグリーンを確認する
プロダクトコードもテストコードもリファクタリングする
→優先するのはプロダクトコード

==== TODOリストを改定
- [ ] 数を文字列に変換する
    - [x] 1を渡すと文字列"1"に変換する => 仮実装
    - [ ] 2を渡すと文字列"2"に変換する => 

新しいテストを書く時は、assertを追加するより、メソッドを増やしたほうが良い
(e2eテストとかのハイコストなテストでは仕方ない)
↑assert文が縦にたくさん並んでいる(アンチパターン)
  ↑アサーションルーレット
  ↑たくさんありすぎて、何を確認したいのかわからないし、テストエラーより後ろのテストが実行されない
  ↑ワンアサーションパーテスト
void _1を渡すと文字列1に変換する() {
  // 準備
  FizzBuzz fizzBuzz = new FizzBuzz();

  // 実行 & 検証
  assertEquals("1", fizzBuzz.stringify(1));
}

void _2を渡すと文字列2に変換する() {
  // 準備
  FizzBuzz fizzBuzz = new FizzBuzz();

  // 実行 & 検証
  assertEquals("2", fizzBuzz.stringify(2));
}
public String stringify(int i) {
  return String.valueOf(i);
}
==== TODOリストを改定
- [x] 数を文字列に変換する
    - [x] 1を渡すと文字列"1"に変換する => 仮実装
    - [x] 2を渡すと文字列"2"に変換する => 三角測量

・リファクタリング
引数名の変更
public String stringify(int num) {
  return String.valueOf(num);
}
テストコードリファクタリング
重複の除去
↑2アウト派と3アウト派
void _1を渡すと文字列1に変換する() {
  // 準備
  FizzBuzz fizzBuzz = new FizzBuzz();

  // 実行 & 検証
  assertEquals("1", fizzBuzz.stringify(1));
}

void _2を渡すと文字列2に変換する() {
  // 準備
  FizzBuzz fizzBuzz = new FizzBuzz();

  // 実行 & 検証
  assertEquals("2", fizzBuzz.stringify(2));
}
==== TODOリストを改定
- [ ] 3の倍数の時は数の代わりに「Fizz」に変換する
@Test
void _1を渡すと文字列1に変換する() {
  // 準備
  FizzBuzz fizzBuzz = new FizzBuzz();

  // 実行 & 検証
  assertEquals("1", fizzBuzz.stringify(1));
}

@Test
void _2を渡すと文字列2に変換する() {
  // 準備
  FizzBuzz fizzBuzz = new FizzBuzz();

  // 実行 & 検証
  assertEquals("2", fizzBuzz.stringify(2));
}

@Test
void _3を渡すと文字列Fizzに変換する() {
  // 準備
  FizzBuzz fizzBuzz = new FizzBuzz();

  // 実行 & 検証
  assertEquals("Fizz", fizzBuzz.stringify(3));
}
public String stringify(int num) {
  if (num == 3) return "fizz";
  return String.valueOf(num);
}
前準備の除去(前準備の重複)
@BeforeEach
void 前準備() {
  // 各種テストメソッド実行前に呼ばれる
  // テスト実行順番はランダム(多分ハッシュで散らしている)
  // ↑テストの熟考順による依存を防ぐ
  // テスト実行時間を減らすため、分散並列で動かす。その時にテスト間に依存があるとできなくなる
}

@Test
void _1を渡すと文字列1に変換する() {
  // 準備
  FizzBuzz fizzBuzz = new FizzBuzz();

  // 実行 & 検証
  assertEquals("1", fizzBuzz.stringify(1));
}

@Test
void _2を渡すと文字列2に変換する() {
  // 準備
  FizzBuzz fizzBuzz = new FizzBuzz();

  // 実行 & 検証
  assertEquals("2", fizzBuzz.stringify(2));
}

@Test
void _3を渡すと文字列Fizzに変換する() {
  // 準備
  FizzBuzz fizzBuzz = new FizzBuzz();

  // 実行 & 検証
  assertEquals("Fizz", fizzBuzz.stringify(3));
}
FizzBuzz fizzBuzz;

@BeforeEach
void 前準備() {
  // 各種テストメソッド実行前に呼ばれる
  // テスト実行順番はランダム(多分ハッシュで散らしている)
  // ↑テストの熟考順による依存を防ぐ
  // テスト実行時間を減らすため、分散並列で動かす。その時にテスト間に依存があるとできなくなる

  fizzBuzz = new FizzBuzz();
}

@Test
void _1を渡すと文字列1に変換する() {
  assertEquals("1", fizzBuzz.stringify(1));
}

@Test
void _2を渡すと文字列2に変換する() {
  assertEquals("2", fizzBuzz.stringify(2));
}

@Test
void _3を渡すと文字列Fizzに変換する() {
  assertEquals("Fizz", fizzBuzz.stringify(3));
}
==== TODOリストを改定
- [ ] 3の倍数の時は数の代わりに「Fizz」に変換する
  - [x] 3を渡すと文字列"Fizz"に変換する -> 仮実装, 本実装
public String stringify(int num) {
  if (num % 3 == 0) return "Fizz";
  return String.valueOf(num);
}
==== TODOリストを改定
- [ ] 5の倍数の時は数の代わりに「Buzz」に変換する -> 明白な実装(自信があるときは仮実装しないですぐ作る)

* 問題を小さく分割する
* 歩幅を調整する
  * テスト→仮実装→三角測量→実装
  * テスト→仮実装→実装
  * テスト→明白な自走

テストコードとプロダクトコードが残っていて3年経過
↑振る舞いはわかるけど、「で?」
↑肝心の仕様がわからない

どこまでやるべきだったのか
* テストメソッド名を詳細に書く
  * 3の倍数の時にFizzを返す__3の時にFizzを返す

TODOのインデントはツリーに見えてくる
インデントされた箇条書き
@DisplayName("FizzBuzz数列を扱うFizzBuzzクラス")
class FizzBuzzTest {

    FizzBuzz fizzBuzz;

    @BeforeEach
    void 前準備() {
        fizzBuzz = new FizzBuzz();
    }

    @Nested
    class stringifyメソッドはintを文字列に変換する {

        @Nested
        class その他の数の時はそのままの文字列表示する {
            @Test
            void _1を渡すと文字列1に変換する() {
                // 準備
                FizzBuzz fizzBuzz = new FizzBuzz();

                // 実行 & 検証
                assertEquals("1", fizzBuzz.stringify(1));
            }

            @Test
            void _2を渡すと文字列2に変換する() {
                // 準備
                FizzBuzz fizzBuzz = new FizzBuzz();

                // 実行 & 検証
                assertEquals("2", fizzBuzz.stringify(2));
            }
        }

        @Nested
        class _3の倍数の時はFizzを表示する {
            @Test
            void _3を渡すと文字列Fizzに変換する() {
                assertEquals("Fizz", fizzBuzz.stringify(3));
            }
        }

        @Nested
        class _5の倍数の時はBuzzを表示する {
            @Test
            void _5を渡すと文字列Buzzに変換する() {
                assertEquals("Buzz", fizzBuzz.stringify(5));
            }
        }

    }
}
↑この機能だけでもJUnit5に乗り換える価値がある!

テストケースの件数に不安
↑なんでその他だけ2パターンあるの?
↑実装した人しかわからない
テストのメンテナンスコスト
↑余計なテストコードは削るべき
↑三角測量のテストコードを削るべきだった?
  ↑メンテナンスコスト最小という観点では正しい?
↑3の倍数、5の倍数の時のテストが倍数のテストになっていない!

======= TODOリスト
- [ ] 3と5の両方の時は代わりに「FizzBuzz」に変換する

テスト対象の数値が微妙
↑境界値でテスト(99, 100, 101)
↑101の時はどういう振る舞い?
↑0の場合は?

テストの構造化とリファクタリング!!!!
↑現在において大事なスキル