Help us understand the problem. What is going on with this article?

いまさらながらSwiftでTDDをやってみた

More than 1 year has passed since last update.

みなさん、こんにちは!

Swift その2 Advent Calendar 2017 19日目の記事です

Swiftでなにか作ろうと思ったのですが、時間が足りず...
そこで、自分がTestをほとんどやったことなかったので、Test系の記事にしました。(ごめんなさい)
初歩の初歩だと思うので、これから始めようと思ってる方向けになりますが、この記事を読んでTestを始めてもらえたら嬉しいです:relaxed:

TDD_説明編

最近、よく耳にするようになって来た言葉ですね
TDDとはtest-driven developmentの略で、テスト駆動開発という開発手法のことを指します。

開発手順

基本的な開発手順は3ステップです。

名称未設定ファイル.jpg

  1. 「Red」:テストを書く
  2. 「Green」:実装する
  3. 「Refactor」:リファクタリングをする

「Red:no_entry:」は、テストを書きますが、実行するとテストが通らない実装を書きます。
「Green:white_check_mark:」は、実装を行なっていきます。実行するとテストを通ります。
「Refactor:arrows_counterclockwise:」は、「Green」の実装を綺麗にしていきます。

※今回、どの手順かわかりやすくするためにそれっぽい色の絵文字つけていきます

TDD_実装編

TDDの開発手順に沿って開発していこうと思います。
今回は、簡単なtodo管理アプリを作成しようと思います。
プロジェクト名はTestable-swiftとしておきます。

Xcodeでテストをする場合、XCTestを使用します。
XCTestについてや、開発の環境の整え方はこちらの記事を参考によろしくお願いします
[iOS] ユニットテストの始め方 〜 テスト環境の作り方とXCTestの使い方 〜

todoを管理するクラスの作成

todoの文字列を格納するArrayを変数として持つだけのクラスを作成します。

TodoList.swift
class TodoList {
    var list = Array<String>()
}

todoの追加

listへtodoを追加するメソッドを作っていきます。

Red:no_entry:

追加するメソッドだけでは、テストができないので、取得するメソッドも使用しましょう。

テストを先に書きます。
追加するメソッドをappend()、最初の値を取得するメソッドをgetTop()とします。

Testable_swiftTests.swift
class Testable_swiftTests: XCTestCase {

    var todoList: TodoList!

    override func setUp() {
        todoList = TodoList()
    }

    func test_TodoAppendAndGet() {
        todoList.append("todo_1")
        XCTAssertEqual("todo_1", todoList.getTop())
    }

}

setUp()TodoListのインスタンスを作って、appendを呼んで、getTopappendと同じ値が取得出来ているのを確認します。
TodoListのインスタンスは他のテストでも使うので、変数にしておきます。

ここまで書くと、エラーが出ます。
TodoListappend()getTop()がないというエラーが出ているので、TodoListに実装していきます。

TodoList.swift
class TodoList {

    var list = Array<String>()

    func append(_ todo: String) {       
    }

    func getTop() -> String {
        return ""
    }

}

append()は、メソッドがあればエラーは無くなるので、空実装。getTop()は返り値が必要なので、空文字列を返しておきます。
テストを失敗させるので、これだけで良いです(笑)

Testを実行して見ましょう:point_up:
Xcodeでは、command + Uでテストを実行できます。

見事、赤になりましたね:no_entry::clap:

Green:white_check_mark:

テストを成功させるためには、getTop()の返り値を変えれば良いですね(笑)
「Green」では、このような簡単な実装で大丈夫です(笑)
単純にテストに成功すれば良いので、これからのこととかはとりあえずここでは考えません(笑)

TodoList.swift
class TodoList {

    var list = Array<String>()

    func append(_ todo: String) {       
    }

    func getTop() -> String {
        return "todo_1"
    }

}

Testを実行して見ましょう:point_up:
テスト成功です:white_check_mark:

Refactor:arrows_counterclockwise:

先ほど実装した、append()getTop()をリファクタリングしていきましょう。
append()で追加した値を、getTop()で返すようにしましょう。

TodoList.swift
class TodoList {

    var list = Array<String>()

    func append(_ todo: String) {
        list.append(todo)
    }

    func getTop() -> String {
        return list[0]
    }

}

Testを実行して見ましょう:point_up:
テスト成功しましたね!
これで、リファクタリングした後のコードも期待した動きをすることが保証されました。

このような手順をメソッドを実装するたびに行なっていくのが、TDDと呼ばれる開発手法です。
TDDでよく使われる手法を紹介していきます:grinning:

TDD_手法

フェイク

単純にテストが成功するように値を入れることを、フェイクと呼びます。
先ほど「Green」で行なった、テストが成功するようにtodo_1という値を入れておくのもフェイクです。

TodoList.swift
func getTop() -> String {
    return "todo_1"
}

例として、listにtodoがあるかどうかを調べるメソッドを追加していきます。

Red:no_entry:

Testable_swiftTests.swift
func test_TodoIsEmpty() {
    XCTAssertTrue(todoList.isEmpty())
}   

テストを失敗させるためにfalseを返しましょう。

TodoList.swift
class TodoList {
    var list = Array<String>()

    func isEmpty() -> Bool {
        return false
    }

}

Testを実行して見ましょう:point_up:
Test失敗!:no_entry:「Red」完了:clap:

Green:white_check_mark:

テストを成功するには、isEmptyからtrueが返ってくればいいので、ここでフェイクとしてtrueを使います!!

TodoList.swift
func isEmpty() -> Bool {
    return true
}

Testを実行して見ましょう:point_up:
成功!!:white_check_mark:

Refactor:arrows_counterclockwise:

ここでは省略します:sob:

トライアンギュレーション(Triangulation)

複数のテストケースを用意し、そのテストケース全てを成功するコードを書くことで、一般解を出す方法です。

例として、listにあるtodoの個数を取得するメソッドを追加していきます。

Red:no_entry:

Testable_swiftTests.swift
func test_TodoAppendAndSize() {
    todoList.append("todo_1")
    XCTAssertEqual(1, todoList.size())
}

todoを1つ追加して、個数を取得して、1つかどうかのテストです。

TodoList.swift
func size() -> Int {
    return 0
}

Testを実行して見ましょう:point_up:
失敗!!:no_entry:

Green:white_check_mark:

TodoList.swift
func size() -> Int {
    return 1
}

Testを実行して見ましょう:point_up:
成功!!:white_check_mark:

ここでトライアンギュレーションを使います。

Red:no_entry:

Testable_swiftTests.swift
func test_TodoAppendAndSize() {
    todoList.append("todo_1")
    XCTAssertEqual(1, todoList.size())
    todoList.append("todo_2")
    XCTAssertEqual(2, todoList.size())
}

todoの追加をもう一度行い、個数が2つになるテストを追加しました。
これで、テストが複数になりましたね!

Testを実行して見ましょう:point_up:
size()1しか返さないので失敗しましたね:no_entry:

Green:white_check_mark:

TodoList.swift
func size() -> Int {
    return list.count
}

一般解として、listの個数を返すようにしました。

Testを実行して見ましょう:point_up:
成功しましたね!!:white_check_mark:

Refactor:arrows_counterclockwise:

ここでは省略します:sob:

テストが1つだと、フェイクで済んでしまいますが、テストが複数個できた場合には対応できません。そこで、トライアンギュレーションを使うことで正しい実装に導きます:innocent:

TDD_結果

TodoList.swift
class TodoList {
    var list = Array<String>()

    func isEmpty() -> Bool {
        return list.count == 0
    }

    func append(_ todo: String) {
        list.append(todo)
    }

    func getTop() throws -> String {
        do {
            try emptyCheck()
        }
        return list[0]
    }

    func size() -> Int {
        return list.count
    }

    func removeTop() throws {
        do {
            try emptyCheck()
        }
        list.remove(at: 0)
    }

    func emptyCheck() throws {
        if isEmpty() {
            throw NSError(domain: "error", code: -1, userInfo: nil)
        }
    }
}
Testable_swiftTests.swift
class TodoList {
    var list = Array<String>()

    func isEmpty() -> Bool {
        return list.count == 0
    }

    func append(_ todo: String) {
        list.append(todo)
    }

    func getTop() throws -> String {
        do {
            try emptyCheck()
        }
        return list[0]
    }

    func size() -> Int {
        return list.count
    }

    func removeTop() throws {
        do {
            try emptyCheck()
        }
        list.remove(at: 0)
    }

    func emptyCheck() throws {
        if isEmpty() {
            throw NSError(domain: "error", code: -1, userInfo: nil)
        }
    }
}

このようなコードができました!
今回は、車窓からのTDDという記事を元にTDDを行なっていきました:grinning:

感想・まとめ

TDDになかなか手を出せずにいたのですが今回やってみました。
メソッド1つ作るにも手順が多く最初は大変ではありましたが、慣れてくると書くのが楽しくなって来ます!
テストケースはこれで足りているのかと不安になったりもしました(笑)
テストが足りてるか不安、逆にテストがあると安心というのはTDDに染まってしまった証拠かもしれませんね(笑)
皆さんもぜひTDDに手を出してみてください:innocent:

参考資料

XCTestのAssert一覧
Swift 3.0 エラー処理入門

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした