RareJob Tech Blog

レアジョブのエンジニア・デザイナーによる技術ブログです

iOSアプリでリファクタリングしたいクラスにユニットテストを導入する時の知見を共有します

開発本部APP・UXチームの玉置(@tamappe)と申します。主にiOSAndroidの運用を担当しています。 担当しているのに最近は専らiOSのみを開発するようになりました。

Swift 3 からSwift 4.2 にリプレイスとリファクタリングした話しを紹介します。

rarejob-tech-dept.hatenablog.com

前回の記事を投稿してからちょうど2ヶ月が経ちました。 この2ヶ月間が非常に内容が濃過ぎて2ヶ月間やってきた内容を振り返るのは非常に難しいです。

また本日で入社から三ヶ月間が経ち研修期間が終わります。 この三ヶ月間でチームメンバーだけでなく様々なメンバーに支えて頂きましたのでとても快適に開発を進めることができています。 来月からはこれまで以上に頑張ってレアジョブアプリを支えていくつもりです。

今回はフリースタイルに文章を書いても良いということでiOSでのユニットテストコードの書き方について紹介したいと思います。 モバイルアプリ開発においてもう避けて通れなくなっているからですね!

Xcodeユニットテストクラスを追加する

どのIDEでもそうですがほとんどの場合、アプリのプロジェクトファイルを作成すると標準のテストクラスが作成されます。 Xcodeの場合もオプションですがUnit TestをTarget に追加するかどうかは最初に決められます。

f:id:qed805:20190613155310p:plain
xcode_unit_test

また、最初にオプションで設定せずにプロジェクトファイルを作成しても後からユニットテスト用のTargetを追加できます。

f:id:qed805:20190613155451p:plain
add_unit_test_target

今回は便宜上UnitTestSampleというプロジェクトファイル名にします。

ユニットテストの書き方について

それでは実際にユニットテストを書いていきます。 Xcodeユニットテスト命名に厳しくないためどのようなクラス名にしてもいいはずですが、 だいたいの命名規約としてテストするクラス名Testsという命名になるかと思います。

今回はUnitTestSampleTestsというユニットテストクラスを使います。 クラスの作成時のUnitTestSampleTestsの中身は以下の通りです。

UnitTestSampleTests

import XCTest
@testable import UnitTestSample // この行を入れないとテストができません。

class UnitTestSampleTests: XCTestCase {

    override func setUp() {
        // このユニットテストクラスを使う場合の初期設定
    }

    override func tearDown() {
        // クラスのテストを終了した時に実行する内容
    }

    func testExample() {
        // サンプルの関数
    }

    func testPerformanceExample() {
        // パフォーマンスを測定する際に使うような関数
        self.measure {

        }
    }

}

今回は特に上記4つを使う場面はありませんので全て削除します。削除しても問題ありません。

UnitTestSampleTests

import XCTest
@testable import UnitTestSample

class UnitTestSampleTests: XCTestCase {

}

試しにユニットテストをしたいのでテストするためのクラスを本番スキームで作成します。

計算する用のクラスとしてCalculationクラスを作成します。

Calculation

import UIKit

class Calculation: NSObject {
    
    func addNumber(a: Int, b: Int) -> Int {
        return a + b
    }

    func minusNumber(a: Int, b: Int) -> Int {
        return a - b
    }
}

足し算用のメソッドaddNumberと引き算用のメソッドminusNumberを作りました。 それぞれのメソッドに引数a, b を設定します。

これらのメソッドが正常に動くかをテストしたいと思います。

UnitTestSampleTestsクラスを修正します。

import XCTest
@testable import UnitTestSample

class UnitTestSampleTests: XCTestCase {
    
    func test_addNumber() {
        let calculation = Calculation()
        
        let testable = calculation.addNumber(a: 1, b: 2)
        XCTAssertEqual(testable, 3)
    }
    
    func test_minusNumber() {
        let calculation = Calculation()
        
        let testable = calculation.minusNumber(a: 5, b: 1)
        XCTAssertEqual(testable, 4)
    }
}

テストの命名は私の慣習でtest_テストする関数名としていますがチーム開発の場合はある程度の規則で命名すれば良いと思います。

この後に「command + B」のショートカットを実行すると行数の部分にダイヤマークが出てきます。

f:id:qed805:20190613161430p:plain
sample_test

ユニットテストの実行は「command + U」で実行できます。

とりあえず、何も考えずに「command + B」してから「command + U」をすればユニットテストができると思います。

ビルドが成功するとTest Succeededと表示されるはずです。

成功した後はダイヤマークが緑色に変わると思います。

f:id:qed805:20190613161800p:plain
test_succeeded

これがユニットテストの基本形です。

iOSエンジニアでユニットテストに書き慣れていない方でしたら 上記コードで見慣れないのがXCTAssertEqualメソッドかと思われます。

これはXCTAssertEqual(A, B)という書き方をしまして、AとBの値が等しいかどうかを確認するメソッドです。 正しくない場合はテストが失敗して赤色のエラーが表示されます。

試しに

UnitTestSampleTests

import XCTest
@testable import UnitTestSample

class UnitTestSampleTests: XCTestCase {
    
    func test_addNumber() {
        let calculation = Calculation()
        
        let testable = calculation.addNumber(a: 1, b: 2)
        XCTAssertEqual(testable, 3)
    }
    
    func test_minusNumber() {
        let calculation = Calculation()
        
        let testable = calculation.minusNumber(a: 5, b: 1)
        XCTAssertEqual(testable, 3)
    }
}

と修正してみて「Command + U」を実行してみましょう。minusNumber に 5, 1 を入れました。 testableには4が入ってますがそれと期待値である3が等しいかどうかの確認です。

当然、4と3は違いますのでテストが失敗します。

f:id:qed805:20190613162323p:plain
test_failed

テストが失敗する場合は緑色から赤色になります。

エラーの内容はXCTAssertEqual failed: ("4") is not equal to ("3")というような感じです。

これで失敗した内容がわかりました。

UnitTest のメソッドの種類について

上記ではXCTAssertEqualのみを使用してきましたが、他にも代表的なテスト用のメソッドがあります。

  • XCTAssertNil(A) (A がnilであるかどうかの確認。Aがnilであれば成功)
  • XCTAssertNotNil(B) (Bがnil以外のオブジェクトかどうかの確認。Bがnilでなければ成功)
  • XCTAssertTrue(C) (C がtrueであるかどうかの確認。Cがtrueであれば成功)
  • XCTAssertFalse(D) (Dがfalseであるかどうかの確認。Dがfalseで成功)
  • XCTAssertNotEqual(E, F) (EとFが等しくないかどうかの確認。EとFが等しくない時に成功)
  • XCTFail() (意図していない挙動の場合にユニットテストを失敗させることができる)

だいたいのテストはこれだけで知りたい内容の確認ができるかなと思います。

import XCTest
@testable import UnitTestSample

class UnitTestSampleTests: XCTestCase {
    
    func test_addNumber() {
        let calculation = Calculation()
        
        let testable = calculation.addNumber(a: 1, b: 2)
        XCTAssertEqual(testable, 3)
    }
    
    func test_minusNumber() {
        let calculation = Calculation()
        
        let testable = calculation.minusNumber(a: 5, b: 1)
        XCTAssertEqual(testable, 4)
    }
    
    func test_nilObject() {
        var nilObject: Int? = nil
        
        XCTAssertNil(nilObject)
        
        nilObject = 1
        XCTAssertNotNil(nilObject)
        
        var isSample = false
        XCTAssertFalse(isSample)
        
        isSample = true
        XCTAssertTrue(isSample)

        let testable = "Hello World"
        XCTAssertEqual(testable, "Hello World")
    }
}

f:id:qed805:20190613163619p:plain
test_sample

他のオブジェクトに関するテストについて

今までは単純な型のユニットでしたが次はオブジェクトのユニットテストについて紹介します。

Calculationクラスに新しい構造体 Userを作成します。

import UIKit

struct User {
    let id: Int
    let name: String
    let number: Int
}

class Calculation: NSObject {
    
    var id: Int = 0
    
    func addNumber(a: Int, b: Int) -> Int {
        return a + b
    }

    func minusNumber(a: Int, b: Int) -> Int {
        return a - b
    }
    
    func createSampleUser(name: String, a: Int, b: Int) -> User {
        let number = addNumber(a: a, b: b)
        let user = User(id: id, name: name, number: number)
        id += 1
        return user
    }
}

こちらのクラスの挙動を確認します。

import XCTest
@testable import UnitTestSample

class UnitTestSampleTests: XCTestCase {

// 省略します。 //
    
    func test_sampleUser() {
        let calculation = Calculation()
        let firstUser = calculation.createSampleUser(name: "太郎", a: 1, b: 2)
        XCTAssertEqual(firstUser.id, 0)
        XCTAssertEqual(firstUser.name, "太郎")
        XCTAssertEqual(firstUser.number, 3)
        
        let secondUser = calculation.createSampleUser(name: "花子", a: 3, b: 6)
        XCTAssertEqual(secondUser.id, 1)
        XCTAssertEqual(secondUser.name, "花子")
        XCTAssertEqual(secondUser.number, 9)
    }
}

これを書いた後に「command + U」をタップしてユニットテストを実行してみましょう。

これでテストが通れば意図通りです。

f:id:qed805:20190613164751p:plain
user_test

テストが通ったことが分かりました。

このようにカスタムなstructclassに対する挙動もユニットテストができるようになります。

レアジョブアプリで導入する予定のクラスについて

レアジョブアプリは2016年にリリースしてからおよそ3年が経過しています。 3年も経過するとコードのメンテナンスが必要である場面が出てきたり、 プラットフォームのAPIのアップデートにより一部分のメソッドが非推奨になる可能性もありえます。

今後レアジョブアプリでユニットテストを施していこうと思うクラスについては

  • Utilityなどの便利クラス
  • extensionクラス
  • API通信で使うModelクラス

このようなクラスのテストを書いていこうと思います。 実際にテストコードを書いてみるとそのクラスのプロパティやメソッドの使い方と結果がわかって動作確認にも便利です。

ぜひ今回の記事を参考にしてユニットテストを書いてみてください。