Xcode、Swift でのユニットテストの試行
この記事について
前の記事 で Xcode でのユニットテストの作り方を確認したので、サンプルのコードで TDD (テスト駆動開発)を行ってみる。
サンプルプログラムの内容
特定の文字列を引数に受け取って、その中に大文字か小文字の"A"(または"a")が含まれる数を返す numberOfA(in: String)
という関数を作る。
プロジェクト名は tddTry
とし、その下の NumberOfA.swift にこの関数を作成する。
テストは tddTryTests/NumberOfATests.swift とする。
コンパイラのエラー(警告)に従ってコードを書く
numberOfA(in: String) のコード(それを含む NumberOfA.swift)よりも先に、テストを書く。
tddTryTests/ の下に NumberOfATests.swift を作成して、最初のテストとして、文字列 Tokyo
を受け取り、"a"の数として 0 が返ってくることを確かめるテストを書く。
2行目に @testable をつけてプロジェクト全体を import しているが、まだ numberOfA(in: String) を書いていないので、scope に numberOfA がないと警告される。(コード内のコメントで表現)
import XCTest
@testable import tddTry
final class NumberOfATests: XCTestCase {
func test_whenGivenTokyo_shouldReturnZero() {
let result = numberOfA(in: "Tokyo") // Cannot find 'numberOfA' in scope
XCTAssertEqual(result, 0)
}
}
プロジェクト(tddTry/) に NumberOfA.swift ファイルを作成し、numberOfA() 関数を記述すると警告は Argument passed to call that takes no arguments
に変わる。
import Foundation
func numberOfA(){
}
String を引数に受け取って、常に Int で 1 を返す、というコードを書くと、とりあえず NuberOfATests.swift はコンパイルが通るようになる。
import Foundation
func numberOfA(in: String) -> Int {
return 1
}
このように、まずは、テストで期待する動作を想定しながら、コンパイルが通るように最低限必要なコードを書いていく。
失敗するテストを書いて修正する
コンパイルは通るようになったが、[command(⌘)] + [U] でテストを実行すると、テストは失敗する。
テスト失敗の警告メッセージは test_whenGivenTokyo_shouldReturnZero(): XCTAssertEqual failed: ("1") is not equal to ("0")
。
numberOfA(in: String) の戻り値の部分を return 0
とすると、このテストは成功するようになる。
条件が異なるテストとして文字列 Chiba
を与えて 1 が戻ってくるテストを追加してテストを実行すると、追加したテストは失敗する。
final class NumberOfATests: XCTestCase {
func test_whenGivenTokyo_shouldReturnZero() {
let result = numberOfA(in: "Tokyo")
XCTAssertEqual(result, 0) // テスト成功
}
func test_whenGivenChiba_shouldReturnOne() {
let result = numberOfA(in: "Chiba")
XCTAssertEqual(result, 1) // テスト失敗
}
}
numberOfA(in: String) を以下のように修正するとテストが通る。
func numberOfA(in string: String) -> Int {
let characterOfA : [Character] = ["a"]
var numberOfA = 0
for character in string {
if characterOfA.contains(character) {
numberOfA += 1
}
}
return numberOfA
}
"a" を複数含む文字列のテストを増やしてもテストは成功する
final class NumberOfATests: XCTestCase {
func test_whenGivenTokyo_shouldReturnZero() {
let result = numberOfA(in: "Tokyo")
XCTAssertEqual(result, 0)
}
func test_whenGivenChiba_shouldReturnOne() {
let result = numberOfA(in: "Chiba")
XCTAssertEqual(result, 1)
}
func test_whenGivenYokohama_shouldReturnTwo() {
let result = numberOfA(in: "Yokohama")
XCTAssertEqual(result, 2)
}
func test_whenGivenSaitama_shouldReturnThree() {
let result = numberOfA(in: "Saitama")
XCTAssertEqual(result, 3)
}
}
しかし、大文字の "A" を含む文字列のテストを追加すると失敗する。
func test_whenGivenAichi_shouldReturnOne() {
let result = numberOfA(in: "Aichi")
XCTAssertEqual(result, 1)
}
テストが成功するように numberOfA(in: String) を修正する
func numberOfA(in string: String) -> Int {
let characterOfA : [Character] = ["a", "A"]
var numberOfA = 0
for character in string {
if characterOfA.contains(character) {
numberOfA += 1
}
}
return numberOfA
}
大文字の "A" を含むテストが通る様になったので、大文字小文字の両方を含むテストを追加してみる。
このテストも成功する。
func test_whenGivenAkita_shouldReturnTwo() {
let result = numberOfA(in: "Akita")
XCTAssertEqual(result, 2)
}
リファクタリングする
都度テストが通ることを確認しながら、コードの重複を省くなどのリファクタリングを行う。
すべてのテストが成功し続ける = 動作が変わっていない、ということを確認しながらリファクタリングを行うことができる。
numberOfA(in: String) を以下のように書き換えても、変わらずにテストが通るので、挙動が変わっていないことを確認できる。
func numberOfA(in string: String) -> Int {
let characterOfA : [Character] = ["a", "A"]
return string.reduce(0) {
$0 + (characterOfA.contains($1) ? 1 : 0)
}
}
まとめ
以下の流れでコーディングをする
- 失敗するテストから書く
- コンパイラが通るように最低限のコードを書く
- テストが失敗することを確認する
- テストが通るようにコードを修正する
- テストを成功させる
- テストが通る状態を保ちながらコードをリファクタリングする
これは、
- 「動作する/しないコード」
- 「綺麗な/汚いコード」
の2軸で考えたときに、最終的に目指すのは「動作する綺麗なコード」だが、いきなりそれを目指したり、「綺麗なコードを書きながら動作する様にしていく」のではなく、まずは**「動作する汚いコード」**で良いので動作を実現してから、綺麗にしていく、という発想。
Discussion