受け入れテスト駆動開発(ATDD)で簡単なiOSアプリを作ってみた

最近、謎の集団zeneloでは、テスト駆動開発(TDD)を徹底しようという風潮が出てきました。
さらには受け入れテスト駆動開発(ATDD)も開発フローに取り入れていきたいという話もしています。
なので、今回は慣れと復習のためATDDでTODOアプリを作る過程をまとめていきます。
(あえて最近全然触っていないiOSで)

# 環境
- Xcode 10.1
- Swift 4.2.1
- XCTest

# 受け入れテスト駆動開発(ATDD)とは？

ATDDでは、先に振る舞い(仕様、要件)をテストコードで定義してからそれを満たす実装を行います。
受け入れテストコード自体が仕様なので、テストがPassしている状態は仕様を満たしていることを意味します。
また、受け入れテストは基本的にユーザーを含め誰もが理解できる自然言語で具体的に書きます。
これには下記のようなメリットがあります。


- 最初に要件を明確化する議論を行うので、ユーザーが求めていることの理解が進む
- 実装完了後に仕様の認識が間違っていたことに気付いて修正をするといった事態が起きにくい
- 受け入れテストが自然言語で具体的に表現されるので、仕様の考慮漏れを減らすことができる

## ATDDのやり方

[Cucumber](https://cucumber.io/)などの受け入れテストツールで採用されている[Gherkin](https://docs.cucumber.io/gherkin/reference/)というテスト記述言語フォーマットが分かりやすいのですが、iOSではGherkinのフォーマットで記述できるツールが見つからなかったので、今回は標準の`XCTest`を使ってGherkin形式っぽく書いていきます。

### 1, 要件を明確化する議論を行い、仕様を自然言語で具体的に表現する

まずはユーザーやプロダクトオーナーと議論して、要件を明確化にします。
その上で具体例で実現したいこと(仕様)をGiven(条件)、When（操作）、Then（期待結果）で表現し、`受け入れテスト仕様`とする

### 2, 受け入れテスト仕様をテストコードに変換してから実装を行う

テスト駆動開発のサイクルと同様に下記のようなサイクルを回していきます。

- Given-When-Thenで表現した仕様をテストコードに変換してテストを失敗させる（`RED`）
- 実装を書いてテストを成功させる（`GREEN`）
- [Code Smell](https://sourcemaking.com/refactoring/smells)を見つけたらリファクタリング(`REFACTOR`)

![alt](https://blog.ippon.tech/content/images/2018/08/TDD_Graphic.jpg)
> 引用 https://blog.ippon.tech/a-roadmap-to-tdd-adoption/

# 実際にATDDでTODOアプリをつくってみる

## 前提

今回はATDDの流れを大まかに体験することに主眼を置くので
-　機能はTODOの一覧表示機能、TODOの登録機能に絞る
-　実装方法の詳しい手順は書かない
-　ユニットテストは省略する(本来は絶対書くべき)

完成形のアプリは[こちら](https://github.com/yuinchirn/todo-ios)

## 1, 仕様を自然言語で具体的に表現する

本来であれば最初にユーザー、プロダクトオーナー、チームメンバーと要件に関して議論をするのですが、今回は僕1人なので自分が考えた仕様を`Given-When-Then`形式でまとめました。

```gherkin
Scenario: Top画面はTODO一覧画面
When TODOアプリを起動すると
Then TODO一覧画面が表示される
```

```gherkin
Scenario: TODO一覧画面からTODO登録画面に遷移できる
Given TODO一覧画面にいる状態で
When ＋ボタンを押すと
Then TODO登録画面が表示される
```

```gherkin
Scenario: TODO登録画面
Given TODO一覧画面にいる状態で
When TODO登録画面に遷移したとき
Then TODO名の入力欄が表示される
And 登録ボタンが表示される
Then TODO一覧ボタン（戻るボタン）が表示される
```

```gherkin
Scenario: TODO登録画面からTODO一覧画面に戻ることができる
Given TODO登録画面に遷移した状態で
When TODO一覧ボタンを押すと
Then TODO一覧画面に遷移する
```

```gherkin
Scenario: TODO登録画面でTODOを登録できる
Given TODO一覧画面に掃除という項目がなく
And TODO登録画面に遷移し
When TODO名の欄に掃除と入力して登録ボタンを押すと
Then TODO一覧画面に遷移し
And TODO一覧に掃除という項目が表示される
```

これで、それぞれの機能で満たすべきであろう仕様を定義できました。
この時点で上記をステークホルダー(ユーザーやプロダクトオーナー等)に見せて求められていることが本当に満たされているかを確認しておきましょう。

## 2, ATDDで実装

### (1/5)Top画面はTODO一覧画面

まずは下記シナリオを実装します。

```gherkin
Scenario: Top画面はTODO一覧画面
When TODOアプリを起動すると
Then TODO一覧画面が表示される
```

XCTestのテストコードに変換します。
（※Cucumberなどのツールを使えばそのままテストコードとして利用できるが、ないので我慢）

```swift:ToListFeature.swift

import XCTest

class TodoFeatures: XCTestCase, TodoSteps {
    func test_Top画面はTODO一覧画面() {
        when_アプリが起動すると()
        then_TODO一覧画面が表示される()
    }
}
```

仕様の見やすさを担保する関係で各Stepの実装を別で管理したいので
TodoSteps.swiftというファイルを作り、上記で書いたメソッドの実態を書きます。


```swift:TodoSteps.swift

import XCTest

protocol TodoSteps {
    // when
    func when_アプリが起動すると()
    // then
    func then_TODO一覧が表示される()
}

extension TodoSteps {
    // when
    func when_アプリが起動すると() {
        XCUIApplication().launch()
    }
    // then
    func then_TODO一覧が表示される() {
       // タイトルがTODO一覧であるNavigationBarが存在することでTODO一覧が表示されていることを確認
        XCTAssertTrue(XCUIApplication().navigationBars["TODO一覧"].exists)
    }
}

```

`test_Top画面はTODO一覧画面()`のテストを実行します。

<img width="266" alt="スクリーンショット 2018-12-16 21.42.24.png" src="https://qiita-image-store.s3.amazonaws.com/0/41892/d07f32d9-41c8-2ce7-fb71-68ffc0c585db.png">


テストが失敗したのでこれで`RED`の状態に。
何も実装していないので当然失敗するはず。期待通りの結果です。

[ここまでのcommit](https://github.com/yuinchirn/todo-ios/commit/a74a6a2bd8bc5a94011ee3857d92073a74cb7cee)

次にテストをPassさせるための実装をします。
アプリ起動時に表示されるViewに、タイトルが`TODO一覧`であるnavigationBarがあればいいので、今回はMain.storyBoardでNavigationControllerを追加して、RootViewControllerが持つNavigationBarのタイトルを`TODO一覧`にします。

<img width="888" alt="スクリーンショット 2018-12-16 21.45.31.png" src="https://qiita-image-store.s3.amazonaws.com/0/41892/282e096b-9ca8-21c4-14b0-e28723510da5.png">


再び`test_Top画面はTODO一覧画面()`のテストを実行します。

<img width="261" alt="スクリーンショット 2018-12-16 21.46.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/41892/5c486f28-33d7-5c4a-2cf1-78c87cc9d918.png">


テストがPassしたので`Green`の状態に。
この段階では特にリファクタリングの必要がなさそうなのでこのシナリオの実装は完了！

[ここまでのcommit](https://github.com/yuinchirn/todo-ios/commit/53fcb9619af019430ac88b93b0c374e6452f0907)

### (2/5)TODO一覧画面からTODO登録画面に遷移できる

続いて、次のシナリオを実装します。

```gherkin
Scenario: TODO一覧画面からTODO登録画面に遷移できる
Given TODO一覧画面にいる状態で
When ＋ボタンを押すと
Then TODO登録画面が表示される
```

まずはシナリオをテストコード化する。

```swift:TodoFeatures.swift
func test_TODO一覧画面からTODO登録画面に遷移できる() {
    given_TODO一覧画面にいる状態で()
    when_＋ボタンを押すと()
    then_TODO登録画面が表示される()
}
```

続いてgivenのテストコードを書きます。

```swift:TodoSteps.swift
func given_TODO一覧画面にいる状態で() {
    XCUIApplication().launch()
    XCTAssertTrue(XCUIApplication().navigationBars["TODO一覧"].exists)
}

// 未実装なのでtestが失敗するようにしておく
func when_＋ボタンを押すと() { XCTFail("Unimplemented") }
func then_TODO登録画面が表示される() { XCTFail("Unimplemented") }
```

`test_TODO一覧画面からTODO登録画面に遷移できる()`のテストを実行します。

<img width="833" alt="スクリーンショット 2018-12-16 22.42.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/41892/042b03b5-d9e5-91d2-1dc0-d4c8a2db6377.png">


`RED`のままですが、未実装の`when_＋ボタンを押すと()`と`then_TODO登録画面が表示される()`で失敗しているだけで、`given_TODO一覧画面にいる状態で()`はPasssしているのでOKです。

続いてwhenのテストコードを書きます。

```swift:TodoSteps.swift
func when_＋ボタンを押すと() {
    XCUIApplication().buttons["Add"].tap()
}
```

テストを実行します。

<img width="865" alt="スクリーンショット 2018-12-16 22.57.19.png" src="https://qiita-image-store.s3.amazonaws.com/0/41892/afb24fec-37c6-2a1c-4b43-074c1180cdd7.png">


`＋`というボタンが存在しないという理由で`RED`の状態になります。

なので次は＋ボタンを作ります。

<img width="797" alt="スクリーンショット 2018-12-16 22.58.39.png" src="https://qiita-image-store.s3.amazonaws.com/0/41892/6e949e8a-cfb1-53de-8e7b-4495c4f1a6a0.png">

テストを実行します。

<img width="824" alt="スクリーンショット 2018-12-16 23.01.31.png" src="https://qiita-image-store.s3.amazonaws.com/0/41892/1dec9f96-8eab-4cba-aee4-a48b71da374f.png">

`RED`のままだが、`when_＋ボタンを押すと()`でのエラーが解消されたのでOK。

続いて`then_TODO登録画面が表示される()`のテストを書きます。

```swift:TodoSteps.swift
func then_TODO登録画面が表示される() {
    XCTAssertTrue(XCUIApplication().navigationBars["TODO登録"].exists)
}
```

テストを実行します。

<img width="842" alt="スクリーンショット 2018-12-16 23.04.34.png" src="https://qiita-image-store.s3.amazonaws.com/0/41892/77f25a6e-91e8-b161-b1e4-245b29e8577d.png">

テストが失敗したので`RED`の状態になります。

[ここまでのcommit](https://github.com/yuinchirn/todo-ios/commit/e4ed42374445842d2b0446132dc81fa153bc042a)

テストがPassするように、
+を押したらTODO登録画面(「TODO登録」というnavigationBarTitleを持つView)に遷移するような実装をします。

storyBoardでViewControllerを追加し、
「＋」ボタンのactionを追加したViewControllerに向けて遷移方法をpushにします。

<img width="890" alt="スクリーンショット 2018-12-16 23.20.42.png" src="https://qiita-image-store.s3.amazonaws.com/0/41892/e35ce2ed-c83c-00bb-be8c-5c14f5cd31df.png">

テストを実行します。

<img width="268" alt="スクリーンショット 2018-12-16 22.15.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/41892/fbdf025c-79f2-5a50-60f1-441f0a06fad0.png">

Passしたので`Green`の状態に。

これで`TODO一覧画面からTODO登録画面に遷移できる`のScenarioが完成！

[ここまでのcommit](https://github.com/yuinchirn/todo-ios/commit/18b4f737637f10efe672823bdcc944a0b0219a4b)

続いて次のシナリオを実装します。

### (3/5)TODO登録画面のフォーム

```gherkin
Scenario: TODO登録画面のフォーム
Given TODO一覧画面にいる状態で
When TODO登録画面に遷移したとき
Then TODO名の入力欄が表示される
And 登録ボタンが表示される
And TODO一覧ボタンが表示される
```

まずはシナリオに対応するテストコードを書きます。

```swift:TodoFeatures.swift
func test_TODO登録画面のフォーム() {
    given_TODO一覧画面にいる状態で()
    when_TODO登録画面に遷移したとき()
    then_TODO名の入力欄が表示される()
    then_登録ボタンが表示される()
    then_TODO一覧ボタン（戻るボタン）が表示される()
}
```

続いて各ステップの操作を書きます。

```swift:TodoSteps.swift
func when_TODO登録画面に遷移したとき() {
    XCUIApplication().buttons["Add"].tap()
    XCTAssertTrue(XCUIApplication().navigationBars["TODO登録"].exists)
}

func then_TODO名の入力欄が表示される() {
    XCTAssertTrue(XCUIApplication().textFields["TODO名"].exists)
}
    
func then_登録ボタンが表示される() {
    XCTAssertTrue(XCUIApplication().buttons["登録"].exists)
}
    
func then_TODO一覧ボタン（戻るボタン）が表示される() {
    XCTAssertTrue(XCUIApplication().buttons["TODO一覧"].exists)
}
```

テストを実行します。

<img width="838" alt="スクリーンショット 2018-12-16 23.47.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/41892/cee7822f-02c6-c453-c797-6468e2f35455.png">

TODO名という入力欄と、登録というボタンが存在しないことが理由で`RED`になります。

[ここまでのcommit](https://github.com/yuinchirn/todo-ios/commit/da0eea6aaea0fdc5f3aa1369935780524c0ab240)

なのでTODO名という入力欄と、登録というボタンを配置します。

<img width="764" alt="スクリーンショット 2018-12-16 23.56.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/41892/e1708bcb-a05b-48fa-7b90-b2d9127faf87.png">

<img width="739" alt="スクリーンショット 2018-12-17 0.01.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/41892/ec7f2e20-b4b2-73c5-5de2-ed84d75f2dc5.png">

テストを実行します。

<img width="268" alt="スクリーンショット 2018-12-17 0.02.40.png" src="https://qiita-image-store.s3.amazonaws.com/0/41892/7bead0cd-53fb-42b5-e28a-a8eb6531269c.png">

Passしたので`Green`の状態に。
これで`TODO登録画面のフォーム`のシナリオが完成！

[ここまでのcommit](https://github.com/yuinchirn/todo-ios/commit/3261a934f8339c442598d29d90aaed5b6dc736a3)

続いて次のシナリオを実装します。

### (4/5)TODO登録画面からTODO一覧画面に戻ることができる

```gherkin
Scenario: TODO登録画面からTODO一覧画面に戻ることができる
Given TODO登録画面に遷移した状態で
When TODO一覧ボタンを押すと
Then TODO一覧画面に遷移する
```

まずはシナリオに対応するテストコードを書きます。

```swift:TodoFeatures.swift
func test_TODO登録画面からTODO一覧画面に戻ることができる() {
    given_TODO登録画面に遷移した状態で()
    when_TODO一覧ボタンを押すと()
    then_TODO一覧画面に遷移する()
}
```

続いて各ステップの操作を書きます。

```swift:TodoSteps.swift
func given_TODO登録画面に遷移した状態で() {
    XCUIApplication().launch()
    XCUIApplication().buttons["Add"].tap()
    XCTAssertTrue(XCUIApplication().navigationBars["TODO登録"].exists)
}

func when_TODO一覧ボタンを押すと() {
    XCUIApplication().buttons["TODO一覧"].tap()
}
    
func then_TODO一覧画面に遷移する() {
    XCTAssertTrue(XCUIApplication().navigationBars["TODO一覧"].exists)
}
```

テストを実行します。

<img width="271" alt="スクリーンショット 2018-12-17 21.40.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/41892/81fb434c-734e-dabb-4404-4d88873e28bd.png">

テストがPassしました。
これはたまたま`(2/5)TODO一覧画面からTODO登録画面に遷移できる`の画面遷移を実装した際に、戻るボタンも実装されていたのでRedにならずにGreenになりました。

[ここまでのcommit](https://github.com/yuinchirn/todo-ios/commit/ebd9b75f544f054962263c56a58379042cca562d)

ここで一旦今まで書いたテストコードを見てみます。

```swift:TodoSteps.swift
extension TodoSteps {
    
    func given_TODO一覧画面にいる状態で() {
        XCUIApplication().launch()
        XCTAssertTrue(XCUIApplication().navigationBars["TODO一覧"].exists)
    }
    func given_TODO登録画面に遷移した状態で() {
        XCUIApplication().launch()
        XCUIApplication().buttons["Add"].tap()
        XCTAssertTrue(XCUIApplication().navigationBars["TODO登録"].exists)
    }
........
    func when_TODO登録画面に遷移したとき() {
        XCUIApplication().buttons["Add"].tap()
        XCTAssertTrue(XCUIApplication().navigationBars["TODO登録"].exists)
    }
    func then_TODO一覧画面が表示される() {
        XCTAssertTrue(XCUIApplication().navigationBars["TODO一覧"].exists)
    }
 
    func then_TODO登録画面が表示される() {
        XCTAssertTrue(XCUIApplication().navigationBars["TODO登録"].exists)
    }
........
    func then_TODO一覧画面に遷移する() {
        XCTAssertTrue(XCUIApplication().navigationBars["TODO一覧"].exists)
    }
}
```

重複が多いので下記のようにメソッドに切り出してテストコードをリファクタリングします。

```swift:TodoSteps.swift
func isInTodoListView() -> Bool {
   return XCUIApplication().navigationBars["TODO一覧"].exists
}
    
func isInTodoAddView() -> Bool {
    return XCUIApplication().navigationBars["TODO登録"].exists
}
    
func tapAddButton() {
    XCUIApplication().buttons["Add"].tap()
}
```

テストを実行します。

<img width="271" alt="スクリーンショット 2018-12-17 21.40.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/41892/dfb4ce8a-1877-9e51-6847-3d52389f88b1.png">

PassしたのでOKです。

[ここまでのcommit](https://github.com/yuinchirn/todo-ios/commit/339087c327f7e98b112dbbc627bb41c89f83fa66)

続いて最後のシナリオを実装します。

### (5/5)TODO登録画面からTODO一覧画面に戻ることができる

```gherkin
Scenario: TODO登録画面でTODOを登録できる
Given TODO一覧画面に掃除という項目がなく
And TODO登録画面に遷移し
When TODO名の欄に掃除と入力して登録ボタンを押すと
Then TODO一覧画面に遷移し
And　TODO一覧に掃除という項目が表示される
```

まずはシナリオに対応するテストコードを書きます。

```swift:TodoFeatures.swift
func test_TODO登録画面でTODOを登録できる() {
    given_TODO一覧画面に掃除という項目がなく()
    given_TODO登録画面に遷移した状態で()
    when_TODO名の欄に掃除と入力して登録ボタンを押すと()
    then_TODO一覧画面に遷移する()
    then_TODO一覧に掃除という項目が表示される()
}
```

```swift:TodoSteps.swift

func exists掃除cell() -> Bool {
    return XCUIApplication().staticTexts["掃除"].exists
}

func given_TODO一覧画面に掃除という項目がなく() {
    XCUIApplication().launch()
    XCTAssertTrue(isInTodoListView())
    XCTAssertFalse(exists掃除cell())
}

func when_TODO名の欄に掃除と入力して登録ボタンを押すと() {
    XCUIApplication().textFields["TODO名"].tap()
    XCUIApplication().textFields["TODO名"].typeText("掃除")
    XCUIApplication().buttons["登録"].tap()
}

func then_TODO一覧画面に遷移する() {
    XCTAssertTrue(isInTodoListView())
}
    
func then_TODO一覧に掃除という項目が表示される() {
    XCTAssertTrue(exists掃除cell())
}
```

テストを実行します。

<img width="860" alt="スクリーンショット 2018-12-18 21.32.23.png" src="https://qiita-image-store.s3.amazonaws.com/0/41892/6c5ec1d2-cd2b-4802-f95c-eb4644bc83a8.png">


失敗したので`Red`の状態に。

[ここまでのcommit](https://github.com/yuinchirn/todo-ios/commit/fd0eb4d48733e30f3f7ee0cb24a5c1ec3c97ab42)


登録ボタンを押した際にTODO一覧画面へ遷移させ、入力された文字がTODO一覧に表示されるようにする実装を行います。
※実装手順は割愛
[コミットを参照](https://github.com/yuinchirn/todo-ios/commit/ce2406a21de677ae2e2b4c73944eae8006cda508)

テストを実行します。

<img width="269" alt="スクリーンショット 2018-12-19 2.14.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/41892/d4ae27e6-af1a-d710-f0cb-769b2c337dc9.png">

Passしました。
これで全ての受け入れテストがPassしたので仕様が満たされたことになります。
実装や解説がだいぶざっくりでしたが、なんとなくATDDの流れが伝われば嬉しいです。

※現状CoreDataのデータ初期化をUITestの前処理で行う事ができなかったので２回目以降のテストが失敗します。
そもそもアプリとテストで別のデータソースを利用し、テストでは毎回初期化するべきだと思うのですが、iOS力が足りないので今回は解決できませんでした。

## 最後に

最後のデータベース周りの扱いに不安が残るのですが、iOSアプリでもATDDのサイクルで開発できそうだなと実感することができました。
ただ今回は単純で機能が少ないアプリの実装だったので良かったのですが、Cucumberのようなツールがないと自動受け入れテスト部分のコードの管理が大変になりそうだなと思いました。iOSアプリ開発でATDDやるのに便利なツール、ライブラリ等があれば教えてください。

久しぶりにiOS触れて楽しかった！