iOS アプリの Unit Test - Swift 編

  • 44
    Like
  • 0
    Comment

以前、 Objective-COCMock についての記事を書きましたが、今回は Swift について書いていきます。

準備

Xcode の使い方やプロジェクトの設定は Objective-C と基本的に同じなのでそちらを参照してください。

Swift_UT_BuildSettings.png

このように Project > Build Settings > Defines Module を YES に設定します。

テストコード

標準の XCTest Framework を使う場合は書き方が Swift になるだけで、Objective-C とほとんど同じです。
違うところは、Objective-C の場合はテストに関連するクラスを個別に import しなければならなかったのが、@testable import sample のようにテスト対象のターケッドを指定するだけでよくなります。

sampleTests.swift
import XCTest

@testable import sample

class sampleTests: XCTestCase, NetworkManagerDelegate {

    var callApiExpectation: XCTestExpectation? = nil

    override func setUp() {
        super.setUp()
        // Put setup code here. This method is called before the invocation of each test method in the class.
    }

    override func tearDown() {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
        super.tearDown()
    }

    func testExample() {
        // This is an example of a functional test case.
        // Use XCTAssert and related functions to verify your tests produce the correct results.
    }

    func testPerformanceExample() {
        // This is an example of a performance test case.
        self.measure {
            // Put the code you want to measure the time of here.
        }
    }

    // MARK: - Reurn value

    /*!
     * 結果を戻り値で返す。
     */
    func testReturnVelue() {
        let manager = NetworkManager()
        let userData = manager.userData()

        // 戻り値を確認する場合は、素直に比較する。
        XCTAssertNotNil(userData)

        XCTAssertEqual(userData.username, "TEST1234")
        XCTAssertEqual(userData.uuid, "CAD34831-E763-45A9-8BA2-31991DCB682B")
        XCTAssertEqual(userData.rank, 1)

        XCTAssertNotNil(userData.latestAccessDate)
    }

    // MARK: - Asynchronous(Delegate)

    /*!
     * 結果を Delegate で返す。
     */
    func testCallApiDelegate() {
        // 非同期処理の完了を監視するオブジェクトを作成
        // 別メソッドになるためメンバ変数を用意
        self.callApiExpectation = self.expectation(description: "CallApiDelegate")

        let manager = NetworkManager()
        manager.delegate = self
        manager.callApi(command: .Update, parameters: nil, completionHandler: nil)

        // 指定秒数待つ
        self.waitForExpectations(timeout: 10, handler: nil)
    }

    func callBackApi(result: [NetworkManager.ApiResutKeys : Any]) {
        // 非同期処理の監視を終了
        self.callApiExpectation?.fulfill()
        // 結果を確認
        XCTAssertNotNil(result)
    }

    // MARK: - Asynchronous(Blocks)

    /*!
     * 非同期処理で結果を Blocks で返す。
     */
    func testImageDownlaodBlocks() {
        // 非同期処理の完了を監視するオブジェクトを作成
        let expectation = self.expectation(description: "CallApiBlocks")

        let manager = NetworkManager()
        manager.delegate = self
        manager.callApi(command: .Update, parameters: nil) { (result, apiResult, error) in
            // 非同期処理の監視を終了
            expectation.fulfill()
            // 結果を確認
            XCTAssertTrue(result)
        }

        // 指定秒数待つ
        self.waitForExpectations(timeout: 10, handler: nil)
    }

    // MARK: - Asynchronous(Notification)

    /*!
     * 結果を Notification で返す。
     */
    func testCallApiNotification() {
        // 通知を監視する
        self.expectation(forNotification: NetworkManager.ApiCallbackNotification.rawValue, object: nil) { (notification) -> Bool in
            // 結果を確認
            XCTAssertNotNil(notification.object)
            let apiResult = notification.object as! [NetworkManager.ApiResutKeys: AnyObject]
            XCTAssertTrue(apiResult[.Status] as! Bool)
            XCTAssertNil(apiResult[.Error])

            // 通知の監視を終了
            return true
        }

        let manager = NetworkManager()
        manager.callApi(command: .Update, parameters: nil, completionHandler: nil)

        // 指定秒数待つ
        self.waitForExpectations(timeout: 10, handler: nil)
    }
}

UI テスト

UI テストも Objective-C と大きな違いはありません。

sampleUITests.swift
import XCTest

@testable import sample

class sampleUITests: XCTestCase {

    override func setUp() {
        super.setUp()

        // Put setup code here. This method is called before the invocation of each test method in the class.

        // In UI tests it is usually best to stop immediately when a failure occurs.
        continueAfterFailure = false
        // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
        XCUIApplication().launch()

        // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
    }

    override func tearDown() {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
        super.tearDown()
    }

    /*!
     * 表示内容を確認する
     */
    func testViewContentsForTestApplication() -> Void {
        // テストアプリケーションを取得
        let app = XCUIApplication()
        // accessibilityIdentifier に ViewIdentifierUsernameLabel が定義されている UILabel を取得
        let labelElement = app.staticTexts["ViewIdentifierUsernameLabel"]
        // UILabel の text を確認
        XCTAssertEqual(labelElement.label, "TEST1234")
    }

    /*!
     * 非同期処理を確認する
     */
    func testCallApi() {
        let app = XCUIApplication()

        // accessibilityIdentifier に ViewIdentifierCallApiButton が定義されている UIButton を取得
        let buttonElement = app.buttons["ViewIdentifierCallApiButton"]
        // ボタンをタップ
        buttonElement.tap()

        // 非同期処理中は View を表示しているので、
        // accessibilityIdentifier に ViewIdentifierLoadingView が定義されている UIView を取得
        let loadingElement = app.otherElements["ViewIdentifierLoadingView"]
        // 非同期処理中に表示されている View が非表示になるまで指定秒数(5秒)待つ。
        let predicate = NSPredicate(format: "exists == NO")
        self.expectation(for: predicate, evaluatedWith: loadingElement, handler: nil)
        self.waitForExpectations(timeout: 5, handler: nil)

        // 非同期処理の結果を確認する
        let labelElement = app.staticTexts["ViewIdentifierUsernameLabel"]
        XCTAssertEqual(labelElement.label, "TEST5678")
    }

    /*!
     * アラートの制御を確認する。
     */
    func testAlertMessageOK() {
        let app = XCUIApplication()

        // accessibilityIdentifier に ViewIdentifierShowAlertViewButton が定義されている UIButton を取得
        let buttonElement = app.buttons["ViewIdentifierShowAlertViewButton"]
        // ボタンをタップ
        buttonElement.tap()

        // アラートが表示されるまで指定秒数(5秒)待つ。
        let predicate = NSPredicate(format:"0 < count")
        self.expectation(for: predicate, evaluatedWith: app.alerts, handler: nil)
        self.waitForExpectations(timeout: 5, handler: nil)

        // アラートは一つしか表示されていないはず。
        XCTAssertEqual(app.alerts.count, 1)

        // 表示されたアラートの OK ボタンをタップ
        let alertElement = app.alerts.element(boundBy: 0)
        let okButtonElement = alertElement.buttons["OK"];
        okButtonElement.tap()

        // 画面の表示内容が変わっていることを確認
        let labelElement = app.staticTexts["ViewIdentifierUsernameLabel"];
        XCTAssertEqual(labelElement.label, "TEST9012")
    }
}

モックについて

Objective-C の場合ですと OCMock のようなモックライブラリがありますが、こちら の記事を参考にすると、Swift の場合は自分で書く必要があります。

実装例

データクラスからデータを取得し、データが存在しているかを返す処理をテストする場合を例にします。

データクラス側は特に意識するところはないです。

DataManager.swift
import UIKit

protocol DataManagerProtocol {
    func selectData(table: String, predicate: NSPredicate?) -> [Any]?
}

class DataManager: NSObject, DataManagerProtocol {
    class var sharedInstance : DataManager {
        struct Static {
            static let instance = DataManager()
        }
        return Static.instance
    }

    public func selectData(table: String, predicate: NSPredicate?) -> [Any]? {
        return []
    }
}

テスト対象のクラスは差し替えたいクラスを外部から指定できるようにする必要があります。
この場合ですと、データクラスのプロパティを用意しています。

BookmarkManager.swift
import UIKit

protocol BookmarkManagerProtocol {
    func hasBookmarkData() -> Bool
}

class BookmarkManager: NSObject {

    // テスト用にデータクラスのプロパティを用意する
    public var dataManager = DataManager.sharedInstance

    func hasBookmarkData() -> Bool {
        let list = dataManager.selectData(table: "Bookmark", predicate: nil)
        guard let bookmarkList = list else {
            return false
        }
        return 0 < bookmarkList.count
    }
}

テストコードはこのようになります。

sampleMockTests.swift
import XCTest

@testable import sample

class sampleMockTests: XCTestCase {

    override func setUp() {
        super.setUp()
        // Put setup code here. This method is called before the invocation of each test method in the class.
    }

    func testMock() {
        // 差し替えたいクラスを継承した、モッククラスを作成
        class MockDataManager: DataManager {
            public override func selectData(table: String, predicate: NSPredicate?) -> [Any]? {
                // データ取得メソッドの内容を差し替える
                return [1, 2, 3]
            }
        }

        let manager = BookmarkManager()
        // データクラスを作成したモッククラスに差し替える
        manager.dataManager = MockDataManager()
        let hasData = manager.hasBookmarkData()
        XCTAssertTrue(hasData)
    }
}

日付を差し替えたい場合はこのようになります。

DateUtility.swift
import UIKit

class SystemDate: NSObject {
    public func currentDate() -> Date {
        return Date()
    }
}

class DateUtility: NSObject {
    var systemDate = SystemDate()

    public func currentDateString() -> String
    {
        let dateFormatter = DateFormatter()
        dateFormatter.locale = Locale(identifier: "en_US_POSIX")
        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss";
        return dateFormatter.string(from: systemDate.currentDate());
    }

    public func tomorrow() -> Date {
        let wCalendar = Calendar(identifier: .gregorian)
        return wCalendar.date(byAdding: .day, value: 1, to: systemDate.currentDate())!
    }
}
sampleMockTests.swift
import XCTest

@testable import sample

class sampleMockTests: XCTestCase {

    override func setUp() {
        super.setUp()
        // Put setup code here. This method is called before the invocation of each test method in the class.
    }

    func testMockCurrentDate() {
        class MockSystemDate: SystemDate {
            public override func currentDate() -> Date {
                let wCalendar = Calendar(identifier: .gregorian)
                return wCalendar.date(from: DateComponents(year: 2017, month: 1, day: 1))!
            }
        }

        let utility = DateUtility()
        utility.systemDate = MockSystemDate()
        let dateString = utility.currentDateString()
        XCTAssertEqual(dateString, "2017-01-01 00:00:00")
    }

    func testMockTomorrow() {
        class MockSystemDate: SystemDate {
            public override func currentDate() -> Date {
                let wCalendar = Calendar(identifier: .gregorian)
                return wCalendar.date(from: DateComponents(year: 2017, month: 1, day: 2))!
            }
        }
        let wCalendar = Calendar(identifier: .gregorian)
        let expectedValue = wCalendar.date(from: DateComponents(year: 2017, month: 1, day: 3))!

        let utility = DateUtility()
        utility.systemDate = MockSystemDate()
        let actualValue = utility.tomorrow()
        XCTAssertEqual(actualValue, expectedValue)
    }
}

Swift でユニットテストを行う場合は、このようにどこをモック化させるかということを意識して実装する必要が出てきます。

Code Coverage

コードカバレッジも Xcode 標準の機能として提供されています。

Enable_Code_Coverage.png

このように scheme editor の Test action にある gather coverage data のチェックボックを有効にした状態でテストを実行すると、テスト結果の Coverage でコードカバレッジの状態を確認できます。

Code_Coverage.png

また、それぞれのクラスでもどの部分がテスト対象になっているかを確認できます。

Code_Coverage_Class.png

Quick

Swift 向けのテストフレームワークとして Quick というものがあります。
Swift が発表された二日後にリリースされたもので、調べてみますと様々な有名なプロジェクトで使われています。

テストコード

sampleQuickTests.swift
import Quick
import Nimble

@testable import sample

class sampleQuickTests: QuickSpec {
    override func spec() {
        describe("DataManager") {
            context("SelectUserData") {
                it("CheckingData") {
                    let manager = DataManager()
                    let userData = manager.selectUserData()

                    expect(userData.username).toNot(beNil())
                    expect(userData.username).to(equal("TEST1234"))
                }
            }
        }
    }
}

このように QuickSpec を継承したテストクラスを作成します。
メインメソッドは spec() となります。
QuickRSpec を参考にしていますので、同様に describecontext でグループ化することができます。
テストコードは it の中に expect({確認対象}).to({期待値}) のように書きます。
テストに失敗すると、このようなメッセージが表示されます。

Quick_Test_Failed.png

参考

https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/01-introduction.html#//apple_ref/doc/uid/TP40014132-CH1-SW1
https://realm.io/jp/news/tryswift-veronica-ray-real-world-mocking-swift/
http://qiita.com/koduki/items/4fde43b68fe450c6a5d8
http://qiita.com/susieyy/items/56457922d3d6bbee21ef
https://github.com/Quick/Quick/tree/master/Documentation/ja