Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

鈴木穂高(Hodaka Suzuki) Twitter @hoddy3190 ● 2014年DeNA新卒入社 ● アプリゲーム開発・運用(2014/08 〜 2018/10) ○ サーバー、クライアント、マスター管理ツール、インフラ整備、 マネジメントなど ● テスト技術チーム - SWET(2018/10 〜) ○ 仕様品質を向上させるための技術的なアプローチ研究 ■ https://speakerdeck.com/hoddy3190/xing-shi-shou-fa-nituitediao-betemita ○ Androidのテスト教育のための活動

Slide 3

Slide 3 text

目次 ● TODOリストに起こそう ● テストを書こう ● 演習 ● テストを書こう(つづき) ● 構造化 ● まとめ ● 演習

Slide 4

Slide 4 text

本資料で使うツール、言語、ライブラリ ● Android Studio 3.3.2 ● kotlin 1.3.21 ● JUnit5 1.4.0 ○ https://junit.org/junit5/docs/current/user-guide/

Slide 5

Slide 5 text

TODOリストに起こそう

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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の現在の中身)

Slide 9

Slide 9 text

まずは細かなタスクに分解 コツ: 先に機能(仕様)の概要を定義し、そこに枝葉をつけていく ドリルダウンの思考 コツ

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

テストを書こう

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

テストファイルを作る クラスに対応するテストファイルを作ってくれる まだMainActivity.ktしか存在しないので、MainActivity上でたたく Cmd + Shift + T

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

落ちるテストを書く internal class FizzBuzzTest { @Test fun onCreate() { assertEquals(1, 2) } } usage: assertEquals(expected, actual) expectedが先に来ることに注意

Slide 23

Slide 23 text

テスト個別実行 あるテストを個別に実行したい場合、 カーソルをそのテスト内に移動させ、上のコマンドをたたく internal class FizzBuzzTest { @Test fun onCreate() { assertEquals(1, 2) } } Ctrl + Shift + R このあたりにカーソルを持ってきて コマンドをたたく

Slide 24

Slide 24 text

もし が効かない場合 Keymapがコンフリクトしている可能性。 [ Preferences ] -> [ Keymap ] でKeymapを変更しよう。 例えば、Ctrl + Shift + Z がよい。 Ctrl + Shift + R 検索ワード

Slide 25

Slide 25 text

しっかり落ちることを確認 意図通りに落ちるということは、 問題なくテストが動いていることを意味する org.openTestj.AssertionFailedError: Expected :1 Actual :2

Slide 26

Slide 26 text

テストを書いていこう internal class FizzBuzzTest { @Test fun onCreate() { assertEquals(1, 2) } } アサーションを削除

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

テストは動く仕様書 internal class FizzBuzzTest { @Test @DisplayName("数を文字列にして返す ") fun intToStringTest() { } } DisplayNameでもOK

Slide 29

Slide 29 text

テストコードをどう書けばよいのかわからない => タスクに具体性が足りていない

Slide 30

Slide 30 text

● [ ] 数を文字列にして返す ○ 1を渡したら文字列"1"を返す ● [ ] 3の倍数のときは数のかわりにFizzと返す ● [ ] 5の倍数のときはBuzzと返す ● [ ] 3と5両方の倍数の場合にはFizzBuzzと返す NEW

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

テストケース修正 internal class FizzBuzzTest { @Test fun `1を渡したら文字列1を返す`() { } } バッククオートで囲むのもよい ※ただ、クラス名で適用したときにテスト結果の表示が変になった

Slide 34

Slide 34 text

ここからどう書いていくか internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { } }

Slide 35

Slide 35 text

3A internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { // Arrange 前準備 // Act 実行 // Assert 検証 } } 前準備、実行、検証

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

プロダクトコードに空のクラスができる class FizzBuzz { }

Slide 53

Slide 53 text

前回見ていた場所に戻る internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { // Arrange 前準備 val fizzbuzz = FizzBuzz() // Act 実行 // Assert 検証 assertEquals("1", fizzbuzz.convert(1)) } } Cmd + [

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

メンバ関数ができる class FizzBuzz { fun convert(i: Int): Any? { } }

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

メンバ関数ができる class FizzBuzz { fun convert(i: Int): String { return "" } } 仮引数はIntにして、返り値はStringにする とりあえず空文字を返しておく

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

考えたこと ● メソッド名 ● クラス名 ● 引数 ● 返り値 一番最初サイクルで書く時は結構考えること多い なるべく小さめの機能を選ぶのがコツ

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

落ちる org.openTestj.AssertionFailedError: Expected :1 Actual : RED

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

テストが通るように修正 class FizzBuzz { fun convert(i: Int): String { return "1" } } テストを通すための最小限の修正 GREEN

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

通った GREEN

Slide 75

Slide 75 text

● [ ] 数を文字列にして返す ○ [ ] 1を渡したら文字列"1"を返す ● [ ] 3の倍数のときは数のかわりにFizzと返す ● [ ] 5の倍数のときはBuzzと返す ● [ ] 3と5両方の倍数の場合にはFizzBuzzと返す

Slide 76

Slide 76 text

三角測量 テストが偶然パスしただけなのではという不安を払拭するために、 2つ以上の入力を使用してテストを書き、 そのテストが予想通りに落ちるかを確かめる

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

● [ ] 数を文字列にして返す ○ [ ] 1を渡したら文字列"1"を返す ○ [ ] 2を渡したら文字列"2"を返す ● [ ] 3の倍数のときは数のかわりにFizzと返す ● [ ] 5の倍数のときはBuzzと返す ● [ ] 3と5両方の倍数の場合にはFizzBuzzと返す NEW

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

アサーションルーレットアンチパターン 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サイクルを正常に回すことができなくなる

Slide 82

Slide 82 text

原則 1 assertion per 1 test

Slide 83

Slide 83 text

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)) } }

Slide 84

Slide 84 text

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)) } } テストを分割して書く

Slide 85

Slide 85 text

テストを回すと落ちる org.openTestj.AssertionFailedError: Expected :2 Actual :1 RED

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

通った GREEN

Slide 89

Slide 89 text

テストコード 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

Slide 90

Slide 90 text

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コメントの削除

Slide 91

Slide 91 text

前回と同じテストを実行 Ctrl + R 前回実行したテスト

Slide 92

Slide 92 text

通った Refactoring

Slide 93

Slide 93 text

@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

Slide 94

Slide 94 text

@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 共通化できるところを 共通化する

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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 追加 追加

Slide 99

Slide 99 text

@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 まずはここだけ削除

Slide 100

Slide 100 text

通った Refactoring

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

通った Refactoring

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

● [ ] 数を文字列にして返す ○ [ ] 1を渡したら文字列"1"を返す ○ [ ] 2を渡したら文字列"2"を返す ● [ ] 3の倍数のときは数のかわりにFizzと返す ● [ ] 5の倍数のときはBuzzと返す ● [ ] 3と5両方の倍数の場合にはFizzBuzzと返す

Slide 105

Slide 105 text

演習

Slide 106

Slide 106 text

● [ ] 数を文字列にして返す ○ [ ] 1を渡したら文字列"1"を返す ○ [ ] 2を渡したら文字列"2"を返す ● [ ] 3の倍数のときは数のかわりにFizzと返す ● [ ] 5の倍数のときはBuzzと返す ● [ ] 3と5両方の倍数の場合にはFizzBuzzと返す 対象 仮実装・三角測量を使いながらやってみよう

Slide 107

Slide 107 text

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)) } } 今段階のテストコード 今段階のプロダクトコード

Slide 108

Slide 108 text

便利なショートカット一覧(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 + [

Slide 109

Slide 109 text

解答例

Slide 110

Slide 110 text

● [ ] 数を文字列にして返す ○ [ ] 1を渡したら文字列"1"を返す ○ [ ] 2を渡したら文字列"2"を返す ● [ ] 3の倍数のときは数のかわりにFizzと返す ○ [ ] 3を渡したら文字列"Fizz"を返す ● [ ] 5の倍数のときはBuzzと返す ● [ ] 3と5両方の倍数の場合にはFizzBuzzと返す NEW

Slide 111

Slide 111 text

internal class FizzBuzzTest { @Test fun _1を渡したら文字列1を返す() { // 省略 } @Test fun _2を渡したら文字列2を返す() { // 省略 } @Test fun _3を渡したら文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(3)) } } 追加

Slide 112

Slide 112 text

落ちる(個別実行) RED

Slide 113

Slide 113 text

テストを通すための最低限の修正(仮実装) class FizzBuzz { fun convert(i: Int): String { if (i == 3) return "Fizz" return i.toString() } }

Slide 114

Slide 114 text

通った GREEN

Slide 115

Slide 115 text

● [ ] 数を文字列にして返す ○ [ ] 1を渡したら文字列"1"を返す ○ [ ] 2を渡したら文字列"2"を返す ● [ ] 3の倍数のときは数のかわりにFizzと返す ○ [ ] 3を渡したら文字列"Fizz"を返す ○ [ ] 6を渡したら文字列"Fizz"を返す ● [ ] 5の倍数のときはBuzzと返す ● [ ] 3と5両方の倍数の場合にはFizzBuzzと返す NEW

Slide 116

Slide 116 text

三角測量 // 省略 @Test fun _3を渡したら文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(3)) } @Test fun _6を渡したら文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(6)) } // 省略 追加

Slide 117

Slide 117 text

落ちる RED

Slide 118

Slide 118 text

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

Slide 119

Slide 119 text

通った GREEN

Slide 120

Slide 120 text

● [ ] 数を文字列にして返す ○ [ ] 1を渡したら文字列"1"を返す ○ [ ] 2を渡したら文字列"2"を返す ● [ ] 3の倍数のときは数のかわりにFizzと返す ○ [ ] 3を渡したら文字列"Fizz"を返す ○ [ ] 6を渡したら文字列"Fizz"を返す ● [ ] 5の倍数のときはBuzzと返す ● [ ] 3と5両方の倍数の場合にはFizzBuzzと返す

Slide 121

Slide 121 text

つづき

Slide 122

Slide 122 text

● [ ] 数を文字列にして返す ○ [ ] 1を渡したら文字列"1"を返す ○ [ ] 2を渡したら文字列"2"を返す ● [ ] 3の倍数のときは数のかわりにFizzと返す ○ [ ] 3を渡したら文字列"Fizz"を返す ○ [ ] 6を渡したら文字列"Fizz"を返す ● [ ] 5の倍数のときはBuzzと返す ● [ ] 3と5両方の倍数の場合にはFizzBuzzと返す 対象

Slide 123

Slide 123 text

... @Test fun _6を渡したら文字列Fizzを返す() { assertEquals("Fizz", fizzbuzz.convert(6)) } @Test fun _5を渡したら文字列Buzzを返す() { assertEquals("Buzz", fizzbuzz.convert(5)) } ... テストケース追加 追加

Slide 124

Slide 124 text

落ちる RED

Slide 125

Slide 125 text

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

Slide 126

Slide 126 text

テストが通るように修正 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) とは 書かなかった

Slide 127

Slide 127 text

明白な実装 テストの書き方や実装の仕方に不安がないときは テストを書いて、見えている実装をそのまま書く

Slide 128

Slide 128 text

通った GREEN

Slide 129

Slide 129 text

構造化

Slide 130

Slide 130 text

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)) }

Slide 131

Slide 131 text

テストは動く仕様書 日本語で仕様書を書くときに、 箇条書きといった見せ方の工夫をするように テストでも同じことをやる

Slide 132

Slide 132 text

テストを構造化 @Nested inner class _3の倍数の場合 { } @Nested inner class _5の倍数の場合 { } @Nested inner class その他の場合 { }

Slide 133

Slide 133 text

@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)) } }

Slide 134

Slide 134 text

すっきり

Slide 135

Slide 135 text

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

Slide 136

Slide 136 text

テストケース数 ● 1つに統一するか ● 2つに統一するか 2つ 1つ 2つ

Slide 137

Slide 137 text

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

Slide 138

Slide 138 text

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

Slide 139

Slide 139 text

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

Slide 140

Slide 140 text

No content

Slide 141

Slide 141 text

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

Slide 142

Slide 142 text

No content

Slide 143

Slide 143 text

● [ ] 数を文字列にして返す ○ [ ] 1を渡したら文字列"1"を返す ○ [ ] 2を渡したら文字列"2"を返す ● [ ] 3の倍数のときは数のかわりにFizzと返す ○ [ ] 3を渡したら文字列"Fizz"を返す ○ [ ] 6を渡したら文字列"Fizz"を返す ● [ ] 5の倍数のときはBuzzと返す ○ [ ] 5を渡したら文字列"Buzz"を返す ○ [ ] 10を渡したら文字列"Buzz"を返す ● [ ] 3と5両方の倍数の場合にはFizzBuzzと返す

Slide 144

Slide 144 text

まとめ

Slide 145

Slide 145 text

まとめ ● 問題を小さく分割する ○ TODOリスト形式など ● 歩幅を適切に選ぶ ○ 不安だったら テスト -> 仮実装 -> 三角測量 -> 実装 ○ やや不安だったら テスト -> 仮実装 -> 実装 ○ 不安がなければ テスト -> 明白な実装 ● テストコードの読み手のことも考えて テストの構造化とリファクタリングも忘れずに

Slide 146

Slide 146 text

リファレンス ● 50分でわかるテスト駆動開発 ○ https://channel9.msdn.com/Events/de-code/2017/DO03?ocid=player ○ 本資料は上の和田卓人(t_wada)さんの動画を参考にしています ○ 動画内でt_wadaさんがFizzBuzzの実装のTDDライブコーディングを 行っていますので、もしご興味あれば是非見てみてください ○ CC BY 4.0に基づいて使用しています ● テスト駆動開発 ○ Kent Beck 著 / 和田卓人 訳 ○ オーム社

Slide 147

Slide 147 text

演習

Slide 148

Slide 148 text

● [ ] 数を文字列にして返す ○ [ ] 1を渡したら文字列"1"を返す ○ [ ] 2を渡したら文字列"2"を返す ● [ ] 3の倍数のときは数のかわりにFizzと返す ○ [ ] 3を渡したら文字列"Fizz"を返す ● [ ] 5の倍数のときはBuzzと返す ○ [ ] 5を渡したら文字列"Buzz"を返す ● [ ] 3と5両方の倍数の場合にはFizzBuzzと返す 対象

Slide 149

Slide 149 text

解答例

Slide 150

Slide 150 text

● [ ] 数を文字列にして返す ○ [ ] 1を渡したら文字列"1"を返す ○ [ ] 2を渡したら文字列"2"を返す ● [ ] 3の倍数のときは数のかわりにFizzと返す ○ [ ] 3を渡したら文字列"Fizz"を返す ○ [ ] 6を渡したら文字列"Fizz"を返す ● [ ] 5の倍数のときはBuzzと返す ○ [ ] 5を渡したら文字列"Buzz"を返す ○ [ ] 10を渡したら文字列"Buzz"を返す ● [ ] 3と5両方の倍数の場合にはFizzBuzzと返す ○ [ ] 15を渡したら文字列"FizzBuzz"を返す ○ [ ] 30を渡したら文字列"FizzBuzz"を返す

Slide 151

Slide 151 text

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

Slide 152

Slide 152 text

落ちる RED

Slide 153

Slide 153 text

テストが通るように修正 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() } }

Slide 154

Slide 154 text

通った GREEN

Slide 155

Slide 155 text

@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の倍数の場合 { // 省略 } 不安だったので追加

Slide 156

Slide 156 text

通った GREEN

Slide 157

Slide 157 text

@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

Slide 158

Slide 158 text

@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 構造化

Slide 159

Slide 159 text

通った Refactoring

Slide 160

Slide 160 text

演習

Slide 161

Slide 161 text

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