




これまでの経緯は、上記記事を参照ください。
Sponsor Link
アプリ設計:振り返り
10フレームでの3投にも対応しましたので、機能的には完成しています。
1フレームでは、最大10ピンまでしか倒せないので、入力もそれに合わせて、入力できるボタンのみを Enable にするようにします。
必要なボタンのみを押せるようにする
ボタンを押せるかテストする
red-green-refactoring のサイクルに従って、ボタンを押せるかどうかのテストから作ります。
テストすべきケースとしては、以下を想定します。
- 1投目は、0〜10の全てのボタンが押せる
- 2投目は、1投目を考慮して合計10になるまでのボタンが押せる(1投目が3ならば、0〜7のボタンが押せる)
- 10フレーム目は、以下の特殊ルール
- 1投目は、全てのボタンが押せる(他のフレームと同じ)
- 2投目は、1投目がストライクでなければ、1投目を考慮して合計10になるまでのボタンが押せる(他のフレームと同じ)
- 2投目は、1投目がストライクでも全てのボタンが押せる(10フレーム向け特別処理)
- 3投目は、2投目がスペアもしくは、ストライクでなければ、全てのボタンが押せない
- 3投目は、1投目ストライク、2投目通常投球であれば、2投目を考慮して合計10になるまでのボタンが押せる
- 3投目は、1,2投目がストライクであれば、全てのボタンが押せる
- 10フレーム終了時には、全てのボタンが押せなくなる
まずは、0〜9フレームでのボタンの Enable/Disable の確認は、以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
func test_buttonAvailable_initial_allShouldBeAvailable() { // because this is first bowl, all button should be available for index in 0...10 { XCTAssertTrue(bowlButtons[index].isEnabled, "Button\(index) should be Enabled") } } func test_buttonAvailablability_afterBowl1_0_AvailabilityDepends() { bowlButtons[0].tap() // still all button should be available for index in 0...10 { XCTAssertTrue(bowlButtons[index].isEnabled, "Button\(index) should be Enabled") } } func test_buttonAvailablability_afterBowl1_4_AvailabilityDepends() { bowlButtons[4].tap() // 0-6 available, 7-10 unavailable for index in 0...6 { XCTAssertTrue(bowlButtons[index].isEnabled, "Button\(index) should be Enabled") } for index in 7...10 { XCTAssertFalse(bowlButtons[index].isEnabled, "Button\(index) should not be Enabled") } } |
10フレームは、少し複雑なので、以下のようにテストが増えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
func test_buttonAvailablability_atFrame10StartWith4Then1_AvailabilityDepends() { // finish 9 frames for _ in 0...8 { bowlButtons[10].tap() } // at 10-th frame bowlButtons[4].tap() for index in 0...6 { XCTAssertTrue(bowlButtons[index].isEnabled, "Button\(index) should be Enabled") } for index in 7...10 { XCTAssertFalse(bowlButtons[index].isEnabled, "Button\(index) should not be Enabled") } bowlButtons[1].tap() for index in 0...10 { XCTAssertFalse(bowlButtons[index].isEnabled, "Button\(index) should not be Enabled") } } func test_buttonAvailablability_atFrame10With4ThenSpareThen3_AvailabilityDepends() { // finish 9 frames for _ in 0...8 { bowlButtons[10].tap() } // at 10-th frame bowlButtons[4].tap() for index in 0...6 { XCTAssertTrue(bowlButtons[index].isEnabled, "Button\(index) should be Enabled") } for index in 7...10 { XCTAssertFalse(bowlButtons[index].isEnabled, "Button\(index) should not be Enabled") } bowlButtons[6].tap() for index in 0...10 { XCTAssertTrue(bowlButtons[index].isEnabled, "Button\(index) should be Enabled") } bowlButtons[3].tap() for index in 0...10 { XCTAssertFalse(bowlButtons[index].isEnabled, "Button\(index) should not be Enabled") } } func test_buttonAvailablability_atFrame10StrikeThen3ThenSpare_AvailabilityDepends() { for _ in 0...8 { bowlButtons[10].tap() } // at 10-th frame bowlButtons[10].tap() for index in 0...10 { XCTAssertTrue(bowlButtons[index].isEnabled, "Button\(index) should be Enabled") } bowlButtons[3].tap() for index in 0...7 { XCTAssertTrue(bowlButtons[index].isEnabled, "Button\(index) should be Enabled") } for index in 8...10 { XCTAssertFalse(bowlButtons[index].isEnabled, "Button\(index) should not be Enabled") } bowlButtons[7].tap() for index in 0...10 { XCTAssertFalse(bowlButtons[index].isEnabled, "Button\(index) should not be Enabled") } } func test_buttonAvailablability_atFrame102StrikeThen3_AvailabilityDepends() { for _ in 0...8 { bowlButtons[10].tap() } // at 10-th frame bowlButtons[10].tap() for index in 0...10 { XCTAssertTrue(bowlButtons[index].isEnabled, "Button\(index) should be Enabled") } bowlButtons[10].tap() for index in 0...10 { XCTAssertTrue(bowlButtons[index].isEnabled, "Button\(index) should be Enabled") } bowlButtons[3].tap() for index in 0...10 { XCTAssertFalse(bowlButtons[index].isEnabled, "Button\(index) should not be Enabled") } } |
これまでに Button を disable にしている処理を入れていないので、Disable であることをテストしている箇所全てで失敗します。
ボタンを Disable にする処理を追加
実装は、Model(BowlingGame) で次の投球で取り得る値の範囲を計算する。Viewは、ViewModel 経由でその情報を使って、各ボタンの Enable/Disable を制御する という方針としました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
func availableRangeForNextThrow() -> Range<Int>? { if let index = findRecordableFrameBowl() { if index.frameIndex != 9 { if index.bowlIndex == 0 { return Range(0...10) } if let prevBowl = bowlResult(frameIndex: index.frameIndex, bowlIndex: 0) { return Range(0...(10-prevBowl)) } } else { let bowl0 = bowlResult(frameIndex: 9, bowlIndex: 0) let bowl1 = bowlResult(frameIndex: 9, bowlIndex: 1) switch index.bowlIndex { case 0: return Range(0...10) case 1: if bowl0 == 10 { return Range(0...10)} return Range(0...(10-bowl0!)) case 2: if (bowl0! == 10)&&(bowl1! == 10) { return Range(0...10)} if (bowl0! == 10)&&(bowl1! < 10) { return Range(0...(10-bowl1!))} if (bowl0! + bowl1! == 10) { return Range(0...10) } default: fatalError() } } } return nil } |
availableRangeForNextThrow は、次の投球ができないのならば、nil を返します。そうでないときは、取り得る値の Range を返します。
この関数の結果を、View の Button は、ViewModel 経由で使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
struct InputView: View { @ObservedObject var viewModel: BowlingGameViewModel var body: some View { HStack { ForEach(0..<11) { index in Button(action: { viewModel.addBowlResult(num: index) }, label: { Text(String(index)) } ) .accessibility(identifier: String("Button\(index)")) .disabled(!viewModel.isButtonAvailable(index: index)) // NEW ! } } .font(.largeTitle) } } |
ViewModel では、以下のように、Model と View を繋いでいます。
1 2 3 4 5 6 7 8 |
func isButtonAvailable(index: Int) -> Bool { if let availableRange = game.availableRangeForNextThrow() { return availableRange.contains(index) } return false } |
これらのコードで、作成したテストすべてをパスすることが確認できます。
最終コード
以下の GitHub で見ることができます。
まとめ その5 完成
TDD(Test Driven Development) 的に、ボーリングスコアアプリを作ってきました。
もう少しテストケースの分析が必要なケースがありそうですが、TDD での開発の雰囲気は 見えてきたのではないでしょうか。
世の中に、もっと TDD での開発が広がると、良いなぁと思ってます。
説明は以上です。
Sponsor Link