LoginSignup
16

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-12-19

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

環境

  • Xcode 10.1
  • Swift 4.2.1
  • XCTest

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

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

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

ATDDのやり方

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

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

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

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

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

  • Given-When-Thenで表現した仕様をテストコードに変換してテストを失敗させる(RED
  • 実装を書いてテストを成功させる(GREEN
  • Code Smellを見つけたらリファクタリング(REFACTOR)

alt

引用 https://blog.ippon.tech/a-roadmap-to-tdd-adoption/

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

前提

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

完成形のアプリはこちら

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

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

Scenario: Top画面はTODO一覧画面
When TODOアプリを起動すると
Then TODO一覧画面が表示される
Scenario: TODO一覧画面からTODO登録画面に遷移できる
Given TODO一覧画面にいる状態で
When +ボタンを押すと
Then TODO登録画面が表示される
Scenario: TODO登録画面
Given TODO一覧画面にいる状態で
When TODO登録画面に遷移したとき
Then TODO名の入力欄が表示される
And 登録ボタンが表示される
Then TODO一覧ボタン(戻るボタン)が表示される
Scenario: TODO登録画面からTODO一覧画面に戻ることができる
Given TODO登録画面に遷移した状態で
When TODO一覧ボタンを押すと
Then TODO一覧画面に遷移する
Scenario: TODO登録画面でTODOを登録できる
Given TODO一覧画面に掃除という項目がなく
And TODO登録画面に遷移し
When TODO名の欄に掃除と入力して登録ボタンを押すと
Then TODO一覧画面に遷移し
And TODO一覧に掃除という項目が表示される

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

2, ATDDで実装

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

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

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

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

ToListFeature.swift

import XCTest

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

仕様の見やすさを担保する関係で各Stepの実装を別で管理したいので
TodoSteps.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一覧画面()のテストを実行します。

スクリーンショット 2018-12-16 21.42.24.png

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

ここまでのcommit

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

スクリーンショット 2018-12-16 21.45.31.png

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

スクリーンショット 2018-12-16 21.46.44.png

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

ここまでのcommit

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

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

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

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

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

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

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登録画面に遷移できる()のテストを実行します。

スクリーンショット 2018-12-16 22.42.56.png

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

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

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

テストを実行します。

スクリーンショット 2018-12-16 22.57.19.png

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

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

スクリーンショット 2018-12-16 22.58.39.png

テストを実行します。

スクリーンショット 2018-12-16 23.01.31.png

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

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

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

テストを実行します。

スクリーンショット 2018-12-16 23.04.34.png

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

ここまでのcommit

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

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

スクリーンショット 2018-12-16 23.20.42.png

テストを実行します。

スクリーンショット 2018-12-16 22.15.16.png

PassしたのでGreenの状態に。

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

ここまでのcommit

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

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

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

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

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

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

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

テストを実行します。

スクリーンショット 2018-12-16 23.47.50.png

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

ここまでのcommit

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

スクリーンショット 2018-12-16 23.56.17.png

スクリーンショット 2018-12-17 0.01.53.png

テストを実行します。

スクリーンショット 2018-12-17 0.02.40.png

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

ここまでのcommit

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

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

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

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

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

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

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

テストを実行します。

スクリーンショット 2018-12-17 21.40.03.png

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

ここまでのcommit

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

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

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

TodoSteps.swift
func isInTodoListView() -> Bool {
   return XCUIApplication().navigationBars["TODO一覧"].exists
}

func isInTodoAddView() -> Bool {
    return XCUIApplication().navigationBars["TODO登録"].exists
}

func tapAddButton() {
    XCUIApplication().buttons["Add"].tap()
}

テストを実行します。

スクリーンショット 2018-12-17 21.40.03.png

PassしたのでOKです。

ここまでのcommit

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

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

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

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

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

テストを実行します。

スクリーンショット 2018-12-18 21.32.23.png

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

ここまでのcommit

登録ボタンを押した際にTODO一覧画面へ遷移させ、入力された文字がTODO一覧に表示されるようにする実装を行います。
※実装手順は割愛
コミットを参照

テストを実行します。

スクリーンショット 2018-12-19 2.14.37.png

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

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

最後に

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

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16