いきなり!TDD(当方テスト初心者でいきなりTDD!) 2日目


はじめに

前回は、TDDの概要と、簡単なinitをサンプルに、

レッド(テストを書く) > グリーン (プロダクトコードを書く) > リファクタリング(コードを整理する)

まで行いました。引き続き、TDDを行なっていきます。

開発環境

  • Xcode 10.3
  • Swift
  • XCTest

作るもの

ポーカー

現在のTODOリスト

[TODOリスト]

  • Cardを定義して、インスタンスを作成する
    • CardはSuitを持つ
    • CardはRankを持つ
  • Cardのインスタンスから文字列表記(notation)を取得する

それでは、最下段のTODOから再開していきましょう。

実装

まずは、レッドからです。TDDPokerBySwiftTests.swiftに以下を追加します。

TDDPokerBySwiftTests.swift
class TDDPokerBySwiftTests: XCTestCase {

   (省略)

   func testCardNotation() {
      let card = Card(suit: .heart, rank: .three)
      XCTAssertEqual(card.notation, "3♥")
   }
}

Command + U でテストを実行してみましょう。

Cardにnotationプロパティが存在していないため、当然エラーになります。これはテストエラーではなく、単純にコンパイルエラーです。

それでは、早速、Cardにnotationプロパティを追加して、テストをグリーンにしていきましょう。

仮実装

Card.swiftに以下を追加します。

Card.swift
struct Card {

   (省略)

    var notation: String {
        return "3♥"
    }
}

Command + U でテストを実行してみましょう。
テストが成功すると思います。このnotationプロパティは、initで代入されるrankとsuitから生成されるものなので、外から代入された値を保持するStored Propertyではなく、Computed Propertyによる実装になっています。

Stored Property (保持型プロパティ)

今回で言う所の、Cardクラスのsuitとrankがそうですね。willSet、didSetなどで監視もでき、lazy修飾詞も使用可能です。

Card.swift
let suit: Suit
let rank: Rank

Computed Property (計算型プロパティ)

それ自体は値を保持せず、setter/getterで値を計算し(set)または返す(get)プロパティです。上記notationの定義がそれになるのですが、上記は、getのみのreadonly定義になっており、get{}が省略されています。省略しない場合には、以下のようになります。

Card.swift
var notation: String {
    get {
        return "3♥"
    }
}

setterを記述する場合には、

var notation: String {
    get {
        return "3♥"
    }

    set(t) {
        print("セット!\(t)");
    }
}

や、

var notation: String {
    get {
        return "3♥"
    }

    set {
        print("セット!\(newValue)");
    }
}

で、notationの値がセットされた時に、setが呼ばれます。set{}内では、Stored Propertyを含めた計算なども可能です。

具体的な使い方の例です。

class Person {
    var birthday: NSDate    // 生年月日
    var age: Int {          // 年齢
        get {
            let components = self.calendar.components(.CalendarUnitYear, fromDate: birthday, toDate: NSDate.date(), options: .allZeros)
            return components.year
        }
        set(newAge) {
            let diff = self.age - newAge
            if diff != 0 {
                self.birthday = self.calendar.dateByAddingUnit(.CalendarUnitYear, value: diff, toDate: self.birthday, options: .allZeros)
            }
        }
    }
    let dateFormatter: NSDateFormatter  // 日付フォーマッタ
    let calendar: NSCalendar            // カレンダー
    // イニシャライザ
    init(birthday: String) {
        self.dateFormatter = NSDateFormatter();
        self.dateFormatter.dateFormat = "Y/M/d"
        self.calendar = NSCalendar.currentCalendar()
        self.birthday = self.dateFormatter.dateFromString(birthday)!
    }
}

let p = Person(birthday: "1980/12/31")
print(p.age)
p.age = p.age - 2
print(p.birthday)     // 1982/12/31

三角測量

話を戻します。現在、notationは"3♥"を静的に返しているだけなので、一般化が保証されてません。テストを修正して、別のケースも追加します。

TDDPokerBySwiftTests.swift
class TDDPokerBySwiftTests: XCTestCase {

    (省略)

    func testCardNotation() {
        let card1 = Card(suit: .heart, rank: .three)
        XCTAssertEqual(card1.notation, "3♥")

        let card2 = Card(suit: .spade, rank: .jack)
        XCTAssertEqual(card2.notation, "J♠")
    }
}

このように2つ以上のテストケースを用意することで、機能の一般化を導き出すテクニックを「三角測量」と言います。テストがグリーンになるように、Card.swiftを書き換えます。enumをStringで定義し直し、各caseに文字列を代入します。取り出す際には、rawValueで取り出せます。

Card.swift
struct Card {

    enum Suit: String {
        case spade = "♠"
        case heart = "♥"
        case club = "♣"
        case diamond = "♦"
    }

    enum Rank: String {
        case ace    = "A"
        case two    = "2"
        case three  = "3"
        case four   = "4"
        case five   = "5"
        case six    = "6"
        case seven  = "7"
        case eight  = "8"
        case nine   = "9"
        case ten    = "10"
        case jack   = "J"
        case queen  = "Q"
        case king   = "K"
    }

    enum CardType {
        case other
    }

    let suit: Suit
    let rank: Rank

    var notation: String {
        return rank.rawValue + suit.rawValue
    }
}

Command + U でテストを実行しましょう。テストが成功すれば、グリーンのステップを終えたことになるので、リファクタリングを行います。今回も、cardの変数をテストケースごとに初期化して、共通利用できるようにするだけです。

TDDPokerBySwiftTests.swift
class TDDPokerBySwiftTests: XCTestCase {

    (省略)

    func testCardNotation() {
        var card: Card

        card = Card(suit: .heart, rank: .three)
        XCTAssertEqual(card.notation, "3♥")

        card = Card(suit: .spade, rank: .jack)
        XCTAssertEqual(card.notation, "J♠")
    }
}

まとめ

今回は、

  • 仮実装
  • 三角測量

について学びました。
次回は、「明白な実装」というものについて、学んで行きたいと思います。

参考

プロパティ
https://tea-leaves.jp/swift/content/%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3