みなさん、こんにちは!
Swift その2 Advent Calendar 2017 19日目の記事です
Swiftでなにか作ろうと思ったのですが、時間が足りず...
そこで、自分がTestをほとんどやったことなかったので、Test系の記事にしました。(ごめんなさい)
初歩の初歩だと思うので、これから始めようと思ってる方向けになりますが、この記事を読んでTestを始めてもらえたら嬉しいです
TDD_説明編
最近、よく耳にするようになって来た言葉ですね
TDDとはtest-driven developmentの略で、テスト駆動開発という開発手法のことを指します。
開発手順
基本的な開発手順は3ステップです。
- 「Red」:テストを書く
- 「Green」:実装する
- 「Refactor」:リファクタリングをする
「Red 」は、テストを書きますが、実行するとテストが通らない実装を書きます。
」は、テストを書きますが、実行するとテストが通らない実装を書きます。
「Green 」は、実装を行なっていきます。実行するとテストを通ります。
」は、実装を行なっていきます。実行するとテストを通ります。
「Refactor 」は、「Green」の実装を綺麗にしていきます。
」は、「Green」の実装を綺麗にしていきます。
※今回、どの手順かわかりやすくするためにそれっぽい色の絵文字つけていきます
TDD_実装編
TDDの開発手順に沿って開発していこうと思います。
今回は、簡単なtodo管理アプリを作成しようと思います。
プロジェクト名はTestable-swiftとしておきます。
Xcodeでテストをする場合、XCTestを使用します。
XCTestについてや、開発の環境の整え方はこちらの記事を参考によろしくお願いします
[iOS] ユニットテストの始め方 〜 テスト環境の作り方とXCTestの使い方 〜
todoを管理するクラスの作成
todoの文字列を格納するArrayを変数として持つだけのクラスを作成します。
class TodoList {
	var list = Array<String>()
}
todoの追加
listへtodoを追加するメソッドを作っていきます。
Red 
追加するメソッドだけでは、テストができないので、取得するメソッドも使用しましょう。
テストを先に書きます。
追加するメソッドをappend()、最初の値を取得するメソッドをgetTop()とします。
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を呼んで、getTopでappendと同じ値が取得出来ているのを確認します。
TodoListのインスタンスは他のテストでも使うので、変数にしておきます。
ここまで書くと、エラーが出ます。
TodoListにappend()とgetTop()がないというエラーが出ているので、TodoListに実装していきます。
class TodoList {
	var list = Array<String>()
	func append(_ todo: String) {		
	}
	
	func getTop() -> String {
		return ""
	}
}
append()は、メソッドがあればエラーは無くなるので、空実装。getTop()は返り値が必要なので、空文字列を返しておきます。
テストを失敗させるので、これだけで良いです(笑)
Testを実行して見ましょう
Xcodeでは、command + Uでテストを実行できます。
見事、赤になりましたね

Green 
テストを成功させるためには、getTop()の返り値を変えれば良いですね(笑)
「Green」では、このような簡単な実装で大丈夫です(笑)
単純にテストに成功すれば良いので、これからのこととかはとりあえずここでは考えません(笑)
class TodoList {
	var list = Array<String>()
	func append(_ todo: String) {		
	}
	func getTop() -> String {
		return "todo_1"
	}
}
Testを実行して見ましょう
テスト成功です
Refactor 
先ほど実装した、append()とgetTop()をリファクタリングしていきましょう。
append()で追加した値を、getTop()で返すようにしましょう。
class TodoList {
	var list = Array<String>()
	func append(_ todo: String) {
		list.append(todo)
	}
	
	func getTop() -> String {
		return list[0]
	}
}
Testを実行して見ましょう
テスト成功しましたね!
これで、リファクタリングした後のコードも期待した動きをすることが保証されました。
このような手順をメソッドを実装するたびに行なっていくのが、TDDと呼ばれる開発手法です。
TDDでよく使われる手法を紹介していきます
TDD_手法
フェイク
単純にテストが成功するように値を入れることを、フェイクと呼びます。
先ほど「Green」で行なった、テストが成功するようにtodo_1という値を入れておくのもフェイクです。
func getTop() -> String {
	return "todo_1"
}
例として、listにtodoがあるかどうかを調べるメソッドを追加していきます。
Red 
func test_TodoIsEmpty() {
	XCTAssertTrue(todoList.isEmpty())
}	
テストを失敗させるためにfalseを返しましょう。
class TodoList {
	var list = Array<String>()
	
	func isEmpty() -> Bool {
		return false
	}
	
}
Testを実行して見ましょう
Test失敗! 「Red」完了
「Red」完了
Green 
テストを成功するには、isEmptyからtrueが返ってくればいいので、ここでフェイクとしてtrueを使います!!
func isEmpty() -> Bool {
	return true
}
Testを実行して見ましょう
成功!!
Refactor 
ここでは省略します
トライアンギュレーション(Triangulation)
複数のテストケースを用意し、そのテストケース全てを成功するコードを書くことで、一般解を出す方法です。
例として、listにあるtodoの個数を取得するメソッドを追加していきます。
Red 
func test_TodoAppendAndSize() {
	todoList.append("todo_1")
	XCTAssertEqual(1, todoList.size())
}
todoを1つ追加して、個数を取得して、1つかどうかのテストです。
func size() -> Int {
	return 0
}
Testを実行して見ましょう
失敗!!
Green 
func size() -> Int {
	return 1
}
Testを実行して見ましょう
成功!!
ここでトライアンギュレーションを使います。
Red 
func test_TodoAppendAndSize() {
	todoList.append("todo_1")
	XCTAssertEqual(1, todoList.size())
	todoList.append("todo_2")
	XCTAssertEqual(2, todoList.size())
}
todoの追加をもう一度行い、個数が2つになるテストを追加しました。
これで、テストが複数になりましたね!
Testを実行して見ましょう
size()が1しか返さないので失敗しましたね
Green 
func size() -> Int {
	return list.count
}
一般解として、listの個数を返すようにしました。
Testを実行して見ましょう
成功しましたね!!
Refactor 
ここでは省略します
テストが1つだと、フェイクで済んでしまいますが、テストが複数個できた場合には対応できません。そこで、トライアンギュレーションを使うことで正しい実装に導きます
TDD_結果
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)
		}
	}
}
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を行なっていきました
感想・まとめ
TDDになかなか手を出せずにいたのですが今回やってみました。
メソッド1つ作るにも手順が多く最初は大変ではありましたが、慣れてくると書くのが楽しくなって来ます!
テストケースはこれで足りているのかと不安になったりもしました(笑)
テストが足りてるか不安、逆にテストがあると安心というのはTDDに染まってしまった証拠かもしれませんね(笑)
皆さんもぜひTDDに手を出してみてください

