Upgrade to Pro — share decks privately, control downloads, hide ads and more …

TDDチュートリアル

 TDDチュートリアル

社内向けにTDDの説明をする際に用いたスライドです。
Android Studioを使ってTDDをチュートリアル形式で紹介しています。
仮実装、三角測量などといったTDDのテクニックを体験していただけるように設計しております。
また、リズムよくTDDを体験してもらえるように、
テスト開発で使えるショートカットの紹介も交えています。

693ed679c8dde3eccbc682ff44f357e1?s=128

Hodaka Suzuki

April 10, 2019
Tweet

Transcript

  1. Apr. 10th 2019 Android Tests Hands-on TDDチュートリアル @hoddy3190

  2. 鈴木穂高(Hodaka Suzuki) Twitter @hoddy3190 • 2014年DeNA新卒入社 • アプリゲーム開発・運用(2014/08 〜 2018/10)

    ◦ サーバー、クライアント、マスター管理ツール、インフラ整備、 マネジメントなど • テスト技術チーム - SWET(2018/10 〜) ◦ 仕様品質を向上させるための技術的なアプローチ研究 ▪ https://speakerdeck.com/hoddy3190/xing-shi-shou-fa-nituitediao-betemita ◦ Androidのテスト教育のための活動
  3. 目次 • TODOリストに起こそう • テストを書こう • 演習 • テストを書こう(つづき) •

    構造化 • まとめ • 演習
  4. 本資料で使うツール、言語、ライブラリ • Android Studio 3.3.2 • kotlin 1.3.21 • JUnit5

    1.4.0 ◦ https://junit.org/junit5/docs/current/user-guide/
  5. TODOリストに起こそう

  6. お題 • 1から100の数が入力されたら、その数の文字列を出力する プログラムを書け。 • ただし3の倍数のときは数の代わりに「Fizz」、 5の倍数のときは「Buzz」を出力しなければならない。 • 3と5両方の倍数の場合には「FizzBuzz」と 出力しなければならない。

  7. 文脈 • フォームに数字を入力し、ボタンを押すと、 FizzBuzzの結果を表示するプログラムを書いている • ボタンを押すと、フォームに入力したテキストを そのまま結果欄に表示させるところまではできた • これからFizzBuzz変換ロジックを書こうとしている 結果欄

    ボタン フォーム
  8. class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) fizzbuzz_button.setOnClickListener { setResultText(convert(fizzbuzz_form.text.toString())) } } private fun convert(input: String): String { // fizzbuzz変換 return input // 仮 } private fun setResultText(str: String) { result_text.text = str } } 文脈(MainActivityの現在の中身)
  9. まずは細かなタスクに分解 コツ: 先に機能(仕様)の概要を定義し、そこに枝葉をつけていく ドリルダウンの思考 コツ

  10. • [ ] 数を文字列にして返す • [ ] 3の倍数のときは数のかわりにFizzと返す • [

    ] 5の倍数のときはBuzzと返す • [ ] 3と5両方の倍数の場合にはFizzBuzzと返す
  11. • [ ] 数を文字列にして返す • [ ] 3の倍数のときは数のかわりにFizzと返す • [

    ] 5の倍数のときはBuzzと返す • [ ] 3と5両方の倍数の場合にはFizzBuzzと返す 概要
  12. • [ ] 数を文字列にして返す • [ ] 3の倍数のときは数のかわりにFizzと返す • [

    ] 5の倍数のときはBuzzと返す • [ ] 3と5両方の倍数の場合にはFizzBuzzと返す 枝葉
  13. • [ ] 数を文字列にして返す • [ ] 3の倍数のときは数のかわりにFizzと返す • [

    ] 5の倍数のときはBuzzと返す • [ ] 3と5両方の倍数の場合にはFizzBuzzと返す この順番で実装
  14. • [ ] 数を文字列にして返す • [ ] 3の倍数のときは数のかわりにFizzと返す • [

    ] 5の倍数のときはBuzzと返す • [ ] 3と5両方の倍数の場合にはFizzBuzzと返す この順番で実装 正常系 準正常系 準正常系 準正常系
  15. テストを書こう

  16. • [ ] 数を文字列にして返す • [ ] 3の倍数のときは数のかわりにFizzと返す • [

    ] 5の倍数のときはBuzzと返す • [ ] 3と5両方の倍数の場合にはFizzBuzzと返す 対象
  17. テストファイルを作る クラスに対応するテストファイルを作ってくれる まだMainActivity.ktしか存在しないので、MainActivity上でたたく Cmd + Shift + T

  18. FizzBuzzTestにする チェックをつける

  19. テストができる internal class FizzBuzzTest { @Test fun onCreate() { }

    }
  20. 落ちるテストを書く internal class FizzBuzzTest { @Test fun onCreate() { assertEquals(1,

    2) } } 落ちるテストを記述
  21. 落ちるテストを書く internal class FizzBuzzTest { @Test fun onCreate() { assertEquals(1,

    2) } } assertEqualsと入力すると候補がたくさん出るが 一番上のものを選んでくれればOK(なんでも良い)
  22. 落ちるテストを書く internal class FizzBuzzTest { @Test fun onCreate() { assertEquals(1,

    2) } } usage: assertEquals(expected, actual) expectedが先に来ることに注意
  23. テスト個別実行 あるテストを個別に実行したい場合、 カーソルをそのテスト内に移動させ、上のコマンドをたたく internal class FizzBuzzTest { @Test fun onCreate()

    { assertEquals(1, 2) } } Ctrl + Shift + R このあたりにカーソルを持ってきて コマンドをたたく
  24. もし が効かない場合 Keymapがコンフリクトしている可能性。 [ Preferences ] -> [ Keymap ]

    でKeymapを変更しよう。 例えば、Ctrl + Shift + Z がよい。 Ctrl + Shift + R 検索ワード
  25. しっかり落ちることを確認 意図通りに落ちるということは、 問題なくテストが動いていることを意味する org.openTestj.AssertionFailedError: Expected :1 Actual :2

  26. テストを書いていこう internal class FizzBuzzTest { @Test fun onCreate() { assertEquals(1,

    2) } } アサーションを削除
  27. テストは動く仕様書 internal class FizzBuzzTest { @Test fun 数を文字列にして返す () {

    } } 日本語で書くのもあり ※ただし、instrumented testでは使えない
  28. テストは動く仕様書 internal class FizzBuzzTest { @Test @DisplayName("数を文字列にして返す ") fun intToStringTest()

    { } } DisplayNameでもOK
  29. テストコードをどう書けばよいのかわからない => タスクに具体性が足りていない

  30. • [ ] 数を文字列にして返す ◦ 1を渡したら文字列"1"を返す • [ ] 3の倍数のときは数のかわりにFizzと返す

    • [ ] 5の倍数のときはBuzzと返す • [ ] 3と5両方の倍数の場合にはFizzBuzzと返す NEW
  31. テストケース修正 internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { }

    }
  32. テストケース修正 internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { }

    } 関数名の最初の文字に数字を使えない
  33. テストケース修正 internal class FizzBuzzTest { @Test fun `1を渡したら文字列1を返す`() { }

    } バッククオートで囲むのもよい ※ただ、クラス名で適用したときにテスト結果の表示が変になった
  34. ここからどう書いていくか internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { }

    }
  35. 3A internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { //

    Arrange 前準備 // Act 実行 // Assert 検証 } } 前準備、実行、検証
  36. アサートファースト • 一番具体的な例からどういう実装をしなければならないかを逆算して考える • 使う人目線でどんなオブジェクトにどんな関数が生えているとよいのかを考える internal class FizzBuzzTest { @Test

    fun _1を渡したら文字列1を返す() { // Arrange 前準備 // Act 実行 // Assert 検証 } } 検証から書こう
  37. アサートファースト • 一番具体的な例からどういう実装をしなければならないかを逆算して考える • 使う人目線でどんなオブジェクトにどんな関数が生えているとよいのかを考える internal class FizzBuzzTest { @Test

    fun _1を渡したら文字列1を返す() { // Arrange 前準備 // Act 実行 // Assert 検証 assertEquals(“1”, ) } } 返り値として文字列を返そう
  38. アサートファースト • 一番具体的な例からどういう実装をしなければならないかを逆算して考える • 使う人目線でどんなオブジェクトにどんな関数が生えているとよいのかを考える internal class FizzBuzzTest { @Test

    fun _1を渡したら文字列1を返す() { // Arrange 前準備 // Act 実行 // Assert 検証 assertEquals(“1”, fizzbuzz) } } fizzbuzzというオブジェクトを用意して
  39. アサートファースト • 一番具体的な例からどういう実装をしなければならないかを逆算して考える • 使う人目線でどんなオブジェクトにどんな関数が生えているとよいのかを考える internal class FizzBuzzTest { @Test

    fun _1を渡したら文字列1を返す() { // Arrange 前準備 // Act 実行 // Assert 検証 assertEquals(“1”, fizzbuzz.convert) } } 数字を文字列に変換する関数が 生えていると使いやすそうだ
  40. アサートファースト • 一番具体的な例からどういう実装をしなければならないかを逆算して考える • 使う人目線でどんなオブジェクトにどんな関数が生えているとよいのかを考える internal class FizzBuzzTest { @Test

    fun _1を渡したら文字列1を返す() { // Arrange 前準備 // Act 実行 // Assert 検証 assertEquals(“1”, fizzbuzz.convert(1)) } } 引数として数字を渡すのはどうだろう
  41. アサートファースト • 一番具体的な例からどういう実装をしなければならないかを逆算して考える • 使う人目線でどんなオブジェクトにどんな関数が生えているとよいのかを考える internal class FizzBuzzTest { @Test

    fun _1を渡したら文字列1を返す() { // Arrange 前準備 // Act 実行 // Assert 検証 assertEquals("1", fizzbuzz.convert(1)) } }
  42. アサートファースト • 一番具体的な例からどういう実装をしなければならないかを逆算して考える • 使う人目線でどんなオブジェクトにどんな関数が生えているとよいのかを考える internal class FizzBuzzTest { @Test

    fun _1を渡したら文字列1を返す() { // Arrange 前準備 // Act 実行 // Assert 検証 assertEquals("1", fizzbuzz.convert(1)) } } コンパイルエラー
  43. ローカル変数作成 エラーが出ている文字の上でたたくと、修正案を 表示してくれる。今回はローカル変数を作る。 internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す()

    { // Arrange 前準備 // Act 実行 // Assert 検証 assertEquals("1", fizzbuzz.convert(1)) } } Opt + Enter
  44. ローカル変数作成 エラーが出ている文字の上でたたくと、修正案を 表示してくれる。今回はローカル変数を作る。 internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す()

    { // Arrange 前準備 // Act 実行 // Assert 検証 val fizzbuzz assertEquals("1", fizzbuzz.convert(1)) } } Opt + Enter 追加される
  45. ローカル変数作成 エラーが出ている文字の上でたたくと、修正案を 表示してくれる。今回はローカル変数を作る。 internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す()

    { // Arrange 前準備 val fizzbuzz // Act 実行 // Assert 検証 assertEquals("1", fizzbuzz.convert(1)) } } Opt + Enter 移動
  46. コンパイルエラー直し internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { //

    Arrange 前準備 val fizzbuzz = FizzBuzz() // Act 実行 // Assert 検証 assertEquals("1", fizzbuzz.convert(1)) } } 初期化処理追加
  47. コンパイルエラー直し internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { //

    Arrange 前準備 val fizzbuzz = FizzBuzz() // Act 実行 // Assert 検証 assertEquals("1", fizzbuzz.convert(1)) } } まだエラー
  48. クラス作成 エラーが出ている文字の上でたたくと、修正案を 表示してくれる。今回はクラスを作る。 Ctrl + Shift + R internal class

    FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { // Arrange 前準備 val fizzbuzz = FizzBuzz() // Act 実行 // Assert 検証 assertEquals("1", fizzbuzz.convert(1)) } } Opt + Enter
  49. クラス作成 エラーが出ている文字の上でたたくと、修正案を 表示してくれる。今回はクラスを作る。 Ctrl + Shift + R internal class

    FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { // Arrange 前準備 val fizzbuzz = FizzBuzz() // Act 実行 // Assert 検証 assertEquals("1", fizzbuzz.convert(1)) } } Opt + Enter
  50. クラス作成 エラーが出ている文字の上でたたくと、修正案を 表示してくれる。今回はクラスを作る。 Ctrl + Shift + R internal class

    FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { // Arrange 前準備 val fizzbuzz = FizzBuzz() // Act 実行 // Assert 検証 assertEquals("1", fizzbuzz.convert(1)) } } Opt + Enter
  51. クラス作成 エラーが出ている文字の上でたたくと、修正案を 表示してくれる。今回はクラスを作る。 Ctrl + Shift + R internal class

    FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { // Arrange 前準備 val fizzbuzz = FizzBuzz() // Act 実行 // Assert 検証 assertEquals("1", fizzbuzz.convert(1)) } } Opt + Enter そのままOK
  52. プロダクトコードに空のクラスができる class FizzBuzz { }

  53. 前回見ていた場所に戻る internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { //

    Arrange 前準備 val fizzbuzz = FizzBuzz() // Act 実行 // Assert 検証 assertEquals("1", fizzbuzz.convert(1)) } } Cmd + [
  54. 行き来が面倒であればsplit windowを使おう

  55. 行き来が面倒であればsplit windowを使おう

  56. 新たなコンパイルエラー internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { //

    Arrange 前準備 val fizzbuzz = FizzBuzz() // Act 実行 // Assert 検証 assertEquals("1", fizzbuzz.convert(1)) } } 新たなコンパイルエラー
  57. また internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { //

    Arrange 前準備 val fizzbuzz = FizzBuzz() // Act 実行 // Assert 検証 assertEquals("1", fizzbuzz.convert(1)) } } Opt + Enter
  58. メンバ関数ができる class FizzBuzz { fun convert(i: Int): Any? { }

    }
  59. メンバ関数ができる class FizzBuzz { fun convert(i: Int): String { return

    "" } }
  60. メンバ関数ができる class FizzBuzz { fun convert(i: Int): String { return

    "" } } 仮引数はIntにして、返り値はStringにする とりあえず空文字を返しておく
  61. ようやくコンパイルエラーがなくなる internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { //

    Arrange 前準備 val fizzbuzz = FizzBuzz() // Act 実行 // Assert 検証 assertEquals("1", fizzbuzz.convert(1)) } }
  62. 考えたこと • メソッド名 • クラス名 • 引数 • 返り値 一番最初サイクルで書く時は結構考えること多い

    なるべく小さめの機能を選ぶのがコツ
  63. テスト実行 internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { //

    Arrange 前準備 val fizzbuzz = FizzBuzz() // Act 実行 // Assert 検証 assertEquals("1", fizzbuzz.convert(1)) } } Ctrl + Shift + R
  64. 落ちる org.openTestj.AssertionFailedError: Expected :1 Actual : RED

  65. 落ちる org.openTestj.AssertionFailedError: Expected :1 Actual : RED でもちゃんとテストがコンパイルされた

  66. テストが通るように修正 class FizzBuzz { fun convert(i: Int): String { return

    "1" } } GREEN
  67. テストが通るように修正 class FizzBuzz { fun convert(i: Int): String { return

    "1" } } テストを通すための最小限の修正 GREEN
  68. 仮実装 いきなり本格的なコードから書き始めると、 テストが失敗したときに、原因がテストコードにあるのか プロダクトコードにあるのかがわかりづらくなるので、 まず簡易的な実装をしてテストを通過させる

  69. テスト駆動開発において テストコードにバグがないことを どうやって保証するか

  70. テストコードのテストコードを書く?

  71. きりがない テストコードのテストコードを書き、 テストコードのテストコードのテストコードを書き、 テストコードのテストコードのテストコードのテストコードを書き...

  72. テストコードのテストは実装側で行う

  73. テスト実行 internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { //

    Arrange 前準備 val fizzbuzz = FizzBuzz() // Act 実行 // Assert 検証 assertEquals("1", fizzbuzz.convert(1)) } } Ctrl + Shift + R
  74. 通った GREEN

  75. • [ ] 数を文字列にして返す ◦ [ ] 1を渡したら文字列"1"を返す • [

    ] 3の倍数のときは数のかわりにFizzと返す • [ ] 5の倍数のときはBuzzと返す • [ ] 3と5両方の倍数の場合にはFizzBuzzと返す
  76. 三角測量 テストが偶然パスしただけなのではという不安を払拭するために、 2つ以上の入力を使用してテストを書き、 そのテストが予想通りに落ちるかを確かめる

  77. 別の入力を使いながら、既存のテストのパスは保ちつつ、 プロダクトコードをリファクタしていく 入出力は明らかなのに内部ロジックが容易にイメージできない実装をする 場合にも有用なテクニック

  78. • [ ] 数を文字列にして返す ◦ [ ] 1を渡したら文字列"1"を返す ◦ [

    ] 2を渡したら文字列"2"を返す • [ ] 3の倍数のときは数のかわりにFizzと返す • [ ] 5の倍数のときはBuzzと返す • [ ] 3と5両方の倍数の場合にはFizzBuzzと返す NEW
  79. 入力が2のときのテストを書こう internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { //

    Arrange 前準備 val fizzbuzz = FizzBuzz() // Act 実行 // Assert 検証 assertEquals("1", fizzbuzz.convert(1)) assertEquals("2", fizzbuzz.convert(2)) } }
  80. 入力が2のときのテストを書こう internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { //

    Arrange 前準備 val fizzbuzz = FizzBuzz() // Act 実行 // Assert 検証 assertEquals("1", fizzbuzz.convert(1)) assertEquals("2", fizzbuzz.convert(2)) } }
  81. アサーションルーレットアンチパターン 1つのテストに複数のアサーション internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() {

    // Arrange 前準備 val fizzbuzz = FizzBuzz() // Act 実行 // Assert 検証 assertEquals("1", fizzbuzz.convert(1)) assertEquals("2", fizzbuzz.convert(2)) } } JUnitの場合、あるアサーションでエラーが出て しまうと、以降のアサーションが実行されず、 TDDサイクルを正常に回すことができなくなる
  82. 原則 1 assertion per 1 test

  83. internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { // Arrange

    前準備 val fizzbuzz = FizzBuzz() // Act 実行 // Assert 検証 assertEquals("1", fizzbuzz.convert(1)) } @Test fun _2を渡したら文字列2を返す() { // Arrange 前準備 val fizzbuzz = FizzBuzz() // Act 実行 // Assert 検証 assertEquals("2", fizzbuzz.convert(2)) } }
  84. internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { // Arrange

    前準備 val fizzbuzz = FizzBuzz() // Act 実行 // Assert 検証 assertEquals("1", fizzbuzz.convert(1)) } @Test fun _2を渡したら文字列2を返す() { // Arrange 前準備 val fizzbuzz = FizzBuzz() // Act 実行 // Assert 検証 assertEquals("2", fizzbuzz.convert(2)) } } テストを分割して書く
  85. テストを回すと落ちる org.openTestj.AssertionFailedError: Expected :2 Actual :1 RED

  86. テストが通るように修正 class FizzBuzz { fun convert(i: Int): String { return

    i.toString() } } GREEN
  87. テストは個別実行ではなく全体実行 プロダクトコードの書き換えにより、 既存のテストの結果が変わることもあるため クリック

  88. 通った GREEN

  89. テストコード internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { val

    fizzbuzz = FizzBuzz() assertEquals("1", fizzbuzz.convert(1)) } @Test fun _2を渡したら文字列2を返す() { val fizzbuzz = FizzBuzz() assertEquals("2", fizzbuzz.convert(2)) } } Refactoring
  90. internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { val fizzbuzz

    = FizzBuzz() assertEquals("1", fizzbuzz.convert(1)) } @Test fun _2を渡したら文字列2を返す() { val fizzbuzz = FizzBuzz() assertEquals("2", fizzbuzz.convert(2)) } } 3Aコメントの削除 テストコード Refactoring 3Aコメントの削除
  91. 前回と同じテストを実行 Ctrl + R 前回実行したテスト

  92. 通った Refactoring

  93. @Test fun _1を渡したら文字列1を返す() { val fizzbuzz = FizzBuzz() assertEquals("1", fizzbuzz.convert(1))

    } @Test fun _2を渡したら文字列2を返す() { val fizzbuzz = FizzBuzz() assertEquals("2", fizzbuzz.convert(2)) } テストコード Refactoring
  94. @Test fun _1を渡したら文字列1を返す() { val fizzbuzz = FizzBuzz() assertEquals("1", fizzbuzz.convert(1))

    } @Test fun _2を渡したら文字列2を返す() { val fizzbuzz = FizzBuzz() assertEquals("2", fizzbuzz.convert(2)) } テストコード Refactoring 共通化できるところを 共通化する
  95. テスト用関数(setUp関数)作成 テストで使うコードのテンプレートから関数を作れる。 カーソルをそのテスト内に移動させ、上のコマンドをたたく。 internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す()

    { ... } Cmd + N このあたりにカーソルを持ってきて コマンドをたたく
  96. テスト用関数(setUp関数)作成 テストで使うコードのテンプレートから関数を作れる カーソルをそのテスト内に移動させ、上のコマンドをたたく internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す()

    { ... } Cmd + N
  97. internal class FizzBuzzTest { @BeforeEach internal fun setUp() { TODO("not

    implemented") // To change body of created functions use File | Settings | File Templates. } @Test fun _1を渡したら文字列1を返す() { val fizzbuzz = FizzBuzz() assertEquals("1", fizzbuzz.convert(1)) } ... setUp関数作成 Refactoring
  98. internal class FizzBuzzTest { private lateinit var fizzbuzz: FizzBuzz @BeforeEach

    internal fun setUp() { fizzbuzz = FizzBuzz() } @Test fun _1を渡したら文字列1を返す() { val fizzbuzz = FizzBuzz() assertEquals("1", fizzbuzz.convert(1)) } ... 共通処理をsetUp関数に集約 Refactoring 追加 追加
  99. @Test fun _1を渡したら文字列1を返す() { val fizzbuzz = FizzBuzz() assertEquals("1", fizzbuzz.convert(1))

    } @Test fun _2を渡したら文字列2を返す() { val fizzbuzz = FizzBuzz() assertEquals("2", fizzbuzz.convert(2)) } 少しずつ直してテスト実行 Refactoring まずはここだけ削除
  100. 通った Refactoring

  101. @Test fun _1を渡したら文字列1を返す() { assertEquals("1", fizzbuzz.convert(1)) } @Test fun _2を渡したら文字列2を返す()

    { val fizzbuzz = FizzBuzz() assertEquals("2", fizzbuzz.convert(2)) } 少しずつ直してテスト実行 Refactoring 次はここを削除
  102. 通った Refactoring

  103. 補足 共通化は、同じ処理が2箇所に現れたら行う派と、 同じ処理が3箇所に現れたら行う派がいる。 今回は、FizzBuzzの簡易性や説明の流れの都合を加味して このタイミングで共通化を行った。

  104. • [ ] 数を文字列にして返す ◦ [ ] 1を渡したら文字列"1"を返す ◦ [

    ] 2を渡したら文字列"2"を返す • [ ] 3の倍数のときは数のかわりにFizzと返す • [ ] 5の倍数のときはBuzzと返す • [ ] 3と5両方の倍数の場合にはFizzBuzzと返す
  105. 演習

  106. • [ ] 数を文字列にして返す ◦ [ ] 1を渡したら文字列"1"を返す ◦ [

    ] 2を渡したら文字列"2"を返す • [ ] 3の倍数のときは数のかわりにFizzと返す • [ ] 5の倍数のときはBuzzと返す • [ ] 3と5両方の倍数の場合にはFizzBuzzと返す 対象 仮実装・三角測量を使いながらやってみよう
  107. class FizzBuzz { fun convert(i: Int): String { return i.toString()

    } } internal class FizzBuzzTest { private lateinit var fizzbuzz: FizzBuzz @BeforeEach internal fun setUp() { fizzbuzz = FizzBuzz() } @Test fun _1を渡したら文字列 1を返す() { assertEquals("1", fizzbuzz.convert(1)) } @Test fun _2を渡したら文字列 2を返す() { assertEquals("2", fizzbuzz.convert(2)) } } 今段階のテストコード 今段階のプロダクトコード
  108. 便利なショートカット一覧(keymap種別: Mac OS X 10.5+) Ctrl + Shift + R

    Cmd + N Opt + Enter Cmd + Shift + T Ctrl + R コードを生成(テスト関数などを作れる) テストファイルを作る テストファイルとプロダクトファイルの行き来をする クイック修正 テストの個別実行 最後に実行したテストの再実行 前見ていた場所に戻る Generate... Test Run ‘hogeTest‘ Run ‘hogeTest‘ コマンド アクション名 説明 Show Intention Actions Back Cmd + [
  109. 解答例

  110. • [ ] 数を文字列にして返す ◦ [ ] 1を渡したら文字列"1"を返す ◦ [

    ] 2を渡したら文字列"2"を返す • [ ] 3の倍数のときは数のかわりにFizzと返す ◦ [ ] 3を渡したら文字列"Fizz"を返す • [ ] 5の倍数のときはBuzzと返す • [ ] 3と5両方の倍数の場合にはFizzBuzzと返す NEW
  111. internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { // 省略

    } @Test fun _2を渡したら文字列2を返す() { // 省略 } @Test fun _3を渡したら文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(3)) } } 追加
  112. 落ちる(個別実行) RED

  113. テストを通すための最低限の修正(仮実装) class FizzBuzz { fun convert(i: Int): String { if

    (i == 3) return "Fizz" return i.toString() } }
  114. 通った GREEN

  115. • [ ] 数を文字列にして返す ◦ [ ] 1を渡したら文字列"1"を返す ◦ [

    ] 2を渡したら文字列"2"を返す • [ ] 3の倍数のときは数のかわりにFizzと返す ◦ [ ] 3を渡したら文字列"Fizz"を返す ◦ [ ] 6を渡したら文字列"Fizz"を返す • [ ] 5の倍数のときはBuzzと返す • [ ] 3と5両方の倍数の場合にはFizzBuzzと返す NEW
  116. 三角測量 // 省略 @Test fun _3を渡したら文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(3)) }

    @Test fun _6を渡したら文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(6)) } // 省略 追加
  117. 落ちる RED

  118. テストが通るように修正 class FizzBuzz { fun convert(i: Int): String { if

    (i % 3 == 0) return "Fizz" return i.toString() } } GREEN
  119. 通った GREEN

  120. • [ ] 数を文字列にして返す ◦ [ ] 1を渡したら文字列"1"を返す ◦ [

    ] 2を渡したら文字列"2"を返す • [ ] 3の倍数のときは数のかわりにFizzと返す ◦ [ ] 3を渡したら文字列"Fizz"を返す ◦ [ ] 6を渡したら文字列"Fizz"を返す • [ ] 5の倍数のときはBuzzと返す • [ ] 3と5両方の倍数の場合にはFizzBuzzと返す
  121. つづき

  122. • [ ] 数を文字列にして返す ◦ [ ] 1を渡したら文字列"1"を返す ◦ [

    ] 2を渡したら文字列"2"を返す • [ ] 3の倍数のときは数のかわりにFizzと返す ◦ [ ] 3を渡したら文字列"Fizz"を返す ◦ [ ] 6を渡したら文字列"Fizz"を返す • [ ] 5の倍数のときはBuzzと返す • [ ] 3と5両方の倍数の場合にはFizzBuzzと返す 対象
  123. ... @Test fun _6を渡したら文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(6)) } @Test fun

    _5を渡したら文字列Buzzを返す() { assertEquals("Buzz", fizzbuzz.convert(5)) } ... テストケース追加 追加
  124. 落ちる RED

  125. テストが通るように修正 class FizzBuzz { fun convert(i: Int): String { if

    (i % 3 == 0) return "Fizz" if (i % 5 == 0) return "Buzz" return i.toString() } }
  126. テストが通るように修正 class FizzBuzz { fun convert(i: Int): String { if

    (i % 3 == 0) return "Fizz" if (i % 5 == 0) return "Buzz" return i.toString() } } あえてif (i == 5) とは 書かなかった
  127. 明白な実装 テストの書き方や実装の仕方に不安がないときは テストを書いて、見えている実装をそのまま書く

  128. 通った GREEN

  129. 構造化

  130. FizzBuzzを一切知らない人がこのテストコードを見たら、 FizzBuzzの挙動がわかるようになっているか @Test fun _1を渡したら文字列1を返す() { assertEquals("1", fizzbuzz.convert(1)) } @Test

    fun _2を渡したら文字列2を返す() { assertEquals("2", fizzbuzz.convert(2)) } @Test fun _3を渡したら文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(3)) } @Test fun _6を渡したら文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(6)) } @Test fun _5を渡したら文字列Buzzを返す() { assertEquals("Buzz", fizzbuzz.convert(5)) }
  131. テストは動く仕様書 日本語で仕様書を書くときに、 箇条書きといった見せ方の工夫をするように テストでも同じことをやる

  132. テストを構造化 @Nested inner class _3の倍数の場合 { } @Nested inner class

    _5の倍数の場合 { } @Nested inner class その他の場合 { }
  133. @Nested inner class _3の倍数の場合 { @Test fun _3を渡したら文字列Fizzを返す() { assertEquals("Fizz",

    fizzbuzz.convert(3)) } @Test fun _6を渡したら文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(6)) } } @Nested inner class _5の倍数の場合 { @Test fun _5を渡したら文字列Buzzを返す() { assertEquals("Buzz", fizzbuzz.convert(5)) } } @Nested inner class その他の場合 { @Test fun _1を渡したら文字列1を返す() { assertEquals("1", fizzbuzz.convert(1)) } @Test fun _2を渡したら文字列2を返す() { assertEquals("2", fizzbuzz.convert(2)) } }
  134. すっきり

  135. テストケースの整理 対称性の乱れは整理のきっかけ。 ※対称性がとれていないからといってそれが悪いわけではない。 ただテストコードを読む際に気にする人もいたりする。 もうひとつ 2つ 1つ 2つ

  136. テストケース数 • 1つに統一するか • 2つに統一するか 2つ 1つ 2つ

  137. どちらがよいのか正解はないし、 統一しないといけないわけでもない

  138. 不安をトレースしたテストケースになっているか 「不安かどうか」というといささか抽象的に感じられるかもしれませんが、 TDDではそういった感情をとても大切にしています。

  139. 不安がない場合 不安が残らないように入力を1つに統一する。 プロダクトコードと同じく、後でテストコードを減らすのは ものすごい大変なので、ここで消しておくメリットもある。

  140. None
  141. 不安がある場合 別のテストケースを加える。 入力を2つに統一するようにテストケース数を調整してもよいし、 対称性にこだわらず、さらにテストケースを追加しても良い。 不安が残らないようにするのが大事。

  142. None
  143. • [ ] 数を文字列にして返す ◦ [ ] 1を渡したら文字列"1"を返す ◦ [

    ] 2を渡したら文字列"2"を返す • [ ] 3の倍数のときは数のかわりにFizzと返す ◦ [ ] 3を渡したら文字列"Fizz"を返す ◦ [ ] 6を渡したら文字列"Fizz"を返す • [ ] 5の倍数のときはBuzzと返す ◦ [ ] 5を渡したら文字列"Buzz"を返す ◦ [ ] 10を渡したら文字列"Buzz"を返す • [ ] 3と5両方の倍数の場合にはFizzBuzzと返す
  144. まとめ

  145. まとめ • 問題を小さく分割する ◦ TODOリスト形式など • 歩幅を適切に選ぶ ◦ 不安だったら テスト

    -> 仮実装 -> 三角測量 -> 実装 ◦ やや不安だったら テスト -> 仮実装 -> 実装 ◦ 不安がなければ テスト -> 明白な実装 • テストコードの読み手のことも考えて テストの構造化とリファクタリングも忘れずに
  146. リファレンス • 50分でわかるテスト駆動開発 ◦ https://channel9.msdn.com/Events/de-code/2017/DO03?ocid=player ◦ 本資料は上の和田卓人(t_wada)さんの動画を参考にしています ◦ 動画内でt_wadaさんがFizzBuzzの実装のTDDライブコーディングを 行っていますので、もしご興味あれば是非見てみてください

    ◦ CC BY 4.0に基づいて使用しています • テスト駆動開発 ◦ Kent Beck 著 / 和田卓人 訳 ◦ オーム社
  147. 演習

  148. • [ ] 数を文字列にして返す ◦ [ ] 1を渡したら文字列"1"を返す ◦ [

    ] 2を渡したら文字列"2"を返す • [ ] 3の倍数のときは数のかわりにFizzと返す ◦ [ ] 3を渡したら文字列"Fizz"を返す • [ ] 5の倍数のときはBuzzと返す ◦ [ ] 5を渡したら文字列"Buzz"を返す • [ ] 3と5両方の倍数の場合にはFizzBuzzと返す 対象
  149. 解答例

  150. • [ ] 数を文字列にして返す ◦ [ ] 1を渡したら文字列"1"を返す ◦ [

    ] 2を渡したら文字列"2"を返す • [ ] 3の倍数のときは数のかわりにFizzと返す ◦ [ ] 3を渡したら文字列"Fizz"を返す ◦ [ ] 6を渡したら文字列"Fizz"を返す • [ ] 5の倍数のときはBuzzと返す ◦ [ ] 5を渡したら文字列"Buzz"を返す ◦ [ ] 10を渡したら文字列"Buzz"を返す • [ ] 3と5両方の倍数の場合にはFizzBuzzと返す ◦ [ ] 15を渡したら文字列"FizzBuzz"を返す ◦ [ ] 30を渡したら文字列"FizzBuzz"を返す
  151. @BeforeEach fun setup() { fizzbuzz = FizzBuzzComp() } @Nested inner

    class _3の倍数かつ5の倍数の場合 { @Test fun _15を渡したら文字列FizzBuzzを返す() { assertEquals("FizzBuzz", fizzbuzz.convert(15)) } } @Nested inner class _3の倍数の場合 { // 省略 }
  152. 落ちる RED

  153. テストが通るように修正 class FizzBuzz { fun convert(i: Int): String { if

    (num % 15 == 0) return "FizzBuzz" if (num % 3 == 0) return "Fizz" if (num % 5 == 0) return "Buzz" return i.toString() } }
  154. 通った GREEN

  155. @Nested inner class _3の倍数かつ5の倍数の場合 { @Test fun _15を渡したら文字列FizzBuzzを返す() { assertEquals("FizzBuzz",

    fizzbuzz.convert(15)) } @Test fun _30を渡したら文字列FizzBuzzを返す() { assertEquals("FizzBuzz", fizzbuzz.convert(30)) } } @Nested inner class _3の倍数の場合 { // 省略 } 不安だったので追加
  156. 通った GREEN

  157. @Nested inner class _3の倍数または5の倍数の場合 { @Nested inner class _3の倍数または5の倍数の場合 {

    @Test fun _15を渡したら文字列 FizzBuzzを返す() { assertEquals("FizzBuzz", fizzbuzz.convert(15)) } @Test fun _30を渡したら文字列 FizzBuzzを返す() { assertEquals("FizzBuzz", fizzbuzz.convert(30)) } } @Nested inner class _3の倍数の場合 { // 省略 } @Nested inner class _5の倍数の場合 { // 省略 } } @Nested inner class その他の場合 { // 省略 } Refactoring
  158. @Nested inner class _3の倍数または5の倍数の場合 { @Nested inner class _3の倍数または5の倍数の場合 {

    @Test fun _15を渡したら文字列 FizzBuzzを返す() { assertEquals("FizzBuzz", fizzbuzz.convert(15)) } @Test fun _30を渡したら文字列 FizzBuzzを返す() { assertEquals("FizzBuzz", fizzbuzz.convert(30)) } } @Nested inner class _3の倍数の場合 { // 省略 } @Nested inner class _5の倍数の場合 { // 省略 } } @Nested inner class その他の場合 { // 省略 } Refactoring 構造化
  159. 通った Refactoring

  160. 演習

  161. TDDBC(TDD Boot Camp) http://devtesting.jp/tddbc/ TDDについて、実習形式で手を動かして体得することを 目的とするイベント。定期的に開催されている。 イベントで使った「お題」についても公開されているので 興味があればどうぞ!