🧑🏻‍💻

Xcode、Swift でのユニットテストの試行

2024/03/16に公開

この記事について

前の記事 で 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 がないと警告される。(コード内のコメントで表現)

NumberOfATests.swift
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 に変わる。

NumberOfA.swift
import Foundation
func numberOfA(){   
}

String を引数に受け取って、常に Int で 1 を返す、というコードを書くと、とりあえず NuberOfATests.swift はコンパイルが通るようになる。

NumberOfA.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 が戻ってくるテストを追加してテストを実行すると、追加したテストは失敗する。

NumberOfATests.swift
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) を以下のように修正するとテストが通る。

NumberOfA.swift
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" を複数含む文字列のテストを増やしてもテストは成功する

NumberOfATests.swift
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" を含む文字列のテストを追加すると失敗する。

NumberOfATests.swift
    func test_whenGivenAichi_shouldReturnOne() {
        let result = numberOfA(in: "Aichi")
        XCTAssertEqual(result, 1)
    }

テストが成功するように numberOfA(in: String) を修正する

NumberOfA.swift
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" を含むテストが通る様になったので、大文字小文字の両方を含むテストを追加してみる。
このテストも成功する。

NumberOfATests.swift
func test_whenGivenAkita_shouldReturnTwo() {
    let result = numberOfA(in: "Akita")
    XCTAssertEqual(result, 2)
}

リファクタリングする

都度テストが通ることを確認しながら、コードの重複を省くなどのリファクタリングを行う。
すべてのテストが成功し続ける = 動作が変わっていない、ということを確認しながらリファクタリングを行うことができる。
numberOfA(in: String) を以下のように書き換えても、変わらずにテストが通るので、挙動が変わっていないことを確認できる。

NumberOfA.swift
func numberOfA(in string: String) -> Int {
    let characterOfA : [Character] = ["a", "A"]
    return string.reduce(0) {
        $0 + (characterOfA.contains($1) ? 1 : 0)
    }
}

まとめ

以下の流れでコーディングをする

  • 失敗するテストから書く
  • コンパイラが通るように最低限のコードを書く
  • テストが失敗することを確認する
  • テストが通るようにコードを修正する
  • テストを成功させる
  • テストが通る状態を保ちながらコードをリファクタリングする

これは、

  • 「動作する/しないコード」
  • 「綺麗な/汚いコード」

の2軸で考えたときに、最終的に目指すのは「動作する綺麗なコード」だが、いきなりそれを目指したり、「綺麗なコードを書きながら動作する様にしていく」のではなく、まずは**「動作する汚いコード」**で良いので動作を実現してから、綺麗にしていく、という発想。

Discussion