11
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

iOSアプリのモックを使った単体テスト

Last updated at Posted at 2018-02-20

単体テストを書こうと思った時、テスト対象のクラスが外部クラスに依存していると非常にテストしにくいです:persevere:
そんな時は、外部クラスをモックに置き換えてしまうとテスト対象クラスだけをテストすることができます:clap:

テスト対象クラス

この記事では、外部依存クラスとしてUIAlertControllerNotificationCenterを含む以下のクラスをテスト対象とします。ボタンが押されたら通知を飛ばすorアラートを表示する非常にシンプルなクラスです。
UIAlertControllerにはshowOkAlert()という拡張メソッドをもたせます。

テスト対象クラスとUIAlertControllerの拡張
class ViewController: UIViewController {
    @IBAction func notify() {
        NotificationCenter.default.post(name: Notification.Name("some"), object: nil)
    }
    
    @IBAction func showAlert() {
        UIAlertController.showOkAlert(vc: self, title: "hoge", message: "fuga")
    }
}

extension UIAlertController {
    static func showOkAlert(vc: UIViewController, title: String, message: String) {
        let alert = UIAlertController(
            title          : title,
            message        : message,
            preferredStyle : .alert
        )
        alert.addAction(UIAlertAction(
            title   : "ok",
            style   : .default,
            handler : nil
        ))
        vc.present(alert, animated: true, completion: nil)
    }
}

モックを作る

早速UIAlertControllerNotificationCenterのモックを作ります。
モックの作り方は2つあります。

プロトコルで作る

元のクラスとモッククラスが持つべきインターフェースをまとめてプロトコルにし、両方に実装させます。
このやり方ならどんなクラスにも適用できるので、基本的にはこちらの方法がオススメです。
UIAlertControllerを例にすると以下の通りです。

UIAlertControllerのモック
import XCTest
@testable import TestSample

// プロトコルの宣言
protocol UIAlertControllerProtocol {
    static func showOkAlert(vc: UIViewController, title: String, message: String)
}

// 元のクラスがプロトコルを実装することを宣言
// UIAlertControllerはもともとshowOkAlert()を持っているのでUIAlertControllerProtocolに適合する
extension UIAlertController: UIAlertControllerProtocol {}

// モッククラスの宣言
// UIAlertControllerProtocolに適合させる
class UIAlertControllerMock: UIAlertControllerProtocol {
    
    // メソッド呼び出しカウント
    static var showOkAlertCallCount = 0
    
    // 引数保存用変数
    static var vc: UIViewController!
    static var title = ""
    static var message = ""
    
    // テストで呼ばれるメソッド
    // 呼び出しカウントを1増やして、引数を保存する
    static func showOkAlert(vc: UIViewController, title: String, message: String) {
        self.showOkAlertCallCount += 1
        self.vc = vc
        self.title = title
        self.message = message
    }
    
    // チェック用メソッド
    // 引数には期待する結果を渡し、実際の値と一致しているか確かめる
    static func check_showOkAlert(vc: UIViewController, title: String, message: String, callCount: Int = 1, file: StaticString = #file, line: UInt = #line) {
        XCTAssertEqual(self.showOkAlertCallCount, callCount, "showOkAlertCallCount", file: file, line: line)
        XCTAssertEqual(self.vc, vc, "vc", file: file, line: line)
        XCTAssertEqual(self.title, title, "title", file: file, line: line)
        XCTAssertEqual(self.message, message, "message", file: file, line: line)
    }
}

モックはテストコードから呼ばれるメソッドを持っており、メソッドが呼ばれた回数と(最新の)引数を保存しています。
テストを実行した後でメソッドの呼び出し回数と引数をチェックすることで、期待通りの動作をしているか確認します。
確認用メソッドはモックに定義しておくと、複数のテストケースで使いまわせて便利です。ただし、アサート系メソッド(上の例ではXCTAssertEqual)をfileやlineの指定なしに書いてしまうと、エラーが起きた際にモック内にエラーが表示されてしまい、どのケースが失敗したのかわかりにくくなってしまいます。
上記の通り、アサート系メソッドにはfileやlineを指定し、メッセージとともにエラーを呼び出し元に返すようにしましょう。

サブクラスで作る

モックを元クラスのサブクラスとして定義することもできます。元クラスのメソッドをオーバーライドしてテスト用のメソッドを作ります。
ただし、extensionで定義されたメソッドはオーバーライドできないという制約があります。拡張メソッドを含む場合はプロトコルを作りましょう。
NotificationCenterを例にすると以下の通りです。

NotificationCenterのモック
import XCTest
@testable import TestSample

// モッククラスの宣言
// NotificationCenterのサブクラスにする
class NotificationCenterMock: NotificationCenter {
    
    // メソッド呼び出しカウント
    var postCallCount = 0
    
    // 引数保存用変数
    var name = NSNotification.Name("")
    
    // テストで呼ばれるメソッド
    // 元クラスのメソッドをオーバーライドして作る
    override func post(name aName: NSNotification.Name, object anObject: Any?) {
        self.postCallCount += 1
        self.name = aName
    }
    
    // チェック用メソッド
    // 引数には期待する結果を渡し、実際の値と一致しているか確かめる
    func check_post(name: Notification.Name, callCount: Int = 1, file: StaticString = #file, line: UInt = #line) {
        XCTAssertEqual(self.postCallCount, callCount, "postCallCount", file: file, line: line)
        XCTAssertEqual(self.name, name, "name", file: file, line: line)
    }
}

メソッドをオーバーライドで定義すること以外は、プロトコルで作るときと同じです。

プロダクトコードの修正

次にテストコードを・・・と行きたいところですが、今のプロダクトコードではテストコードから外部依存クラスをモックに置き換えることができません。
テストができるようにプロダクトコードを修正します。

NotificationCenter.defaultなりUIAlertControllerなりを直接使ってしまっているので、これらをプロパティにします。

修正案1
class ViewController: UIViewController {

    // プロパティ化
    var notificationCenter: NotificationCenter = NotificationCenter.default
    var alertController: UIAlertControllerProtocol.Type = UIAlertController.self

    @IBAction func notify() {
        // プロパティ経由で呼ぶ
        self.notificationCenter.post(name: Notification.Name("some"), object: nil)
    }
    
    @IBAction func showAlert() {
        // プロパティ経由で呼ぶ
        self.alertController.showOkAlert(vc: self, title: "hoge", message: "fuga")
    }
}

staticメソッド呼び出ししているところはプロパティ化できるの?と思われるかもしれませんが、できます。
上記の例のように、型を[型名].Type、値を[型名].selfとすることでプロパティとして持つことができます。わざわざシングルトンにしなくても大丈夫です。

もしプロパティを公開することに抵抗があるなら、セッターを作ってもいいかもしれません。

修正案2
class ViewController: UIViewController {

    // プロパティはprivateをつける
    private var notificationCenter: NotificationCenter = NotificationCenter.default
    private var alertController: UIAlertControllerProtocol.Type = UIAlertController.self

    // セッター
    func setNotificationCenter(notificationCenter: NotificationCenter) {
        self.notificationCenter = notificationCenter
    }
    func setAlertController(alertController: UIAlertControllerProtocol.Type) {
        self.alertController = alertController
    }

    @IBAction func notify() {
        // プロパティ経由で呼ぶ
        self.notificationCenter.post(name: Notification.Name("some"), object: nil)
    }
    
    @IBAction func showAlert() {
        // プロパティ経由で呼ぶ
        self.alertController.showOkAlert(vc: self, title: "hoge", message: "fuga")
    }
}

イニシャライザ的なメソッドを作り、その引数で渡すというのも良いです。

修正案3
class ViewController: UIViewController {

    // プロパティはprivateをつける
    private var notificationCenter: NotificationCenter = NotificationCenter.default
    private var alertController: UIAlertControllerProtocol.Type = UIAlertController.self

    // イニシャライザ的メソッド
    static func assemble(notificationCenter: NotificationCenter, alertController: UIAlertControllerProtocol.Type) -> ViewController {
        let vc = ViewController()
        vc.notificationCenter = notificationCenter
        vc.alertController    = alertController
        return vc
    }

    @IBAction func notify() {
        // プロパティ経由で呼ぶ
        self.notificationCenter.post(name: Notification.Name("some"), object: nil)
    }
    
    @IBAction func showAlert() {
        // プロパティ経由で呼ぶ
        self.alertController.showOkAlert(vc: self, title: "hoge", message: "fuga")
    }
}

以下は修正案3を採用したとして記事を進めます。

モックを使ってテストする

それではいよいよテストを書きましょう。
テストではViewControllernotify()showAlert()が期待通りの挙動をしているか確かめます。あまりにシンプルなのでテストする意味が無いように見えてしまいますが・・・

テストコード
import XCTest
@testable import TestSample

class ViewControllerTests: XCTestCase {

    // テスト対象クラス
    var vc: ViewController!

    // モッククラス
    var notificationCenter: NotificationCenterMock!
    var alertController: UIAlertControllerMock.Type!

    // 各テストケースの前に実行される
    override func setUp() {
        super.setUp()
        self.notificationCenter = NotificationCenterMock()
        self.alertController    = UIAlertControllerMock.self
        self.vc = ViewController.assemble(notificationCenter: self.notificationCenter, alertController: self.alertController)
    }

    // notify()のテスト
    func test_notify() {

        // テスト対象メソッドを叩く
        self.vc.notify()

        // チェック
        self.notificationCenter.check_post(name: "some")
    }

    // showAlert()のテスト
    func test_showAlert() {

        // テスト対象メソッドを叩く
        self.vc.showAlert()

        // チェック
        check_showOkAlert(vc: self.vc, title: "hoge", message: "fuga")
    }
}

テストの流れは、各テストケースの前にsetUp()でテスト対象クラスとモックを初期状態にしてから、テスト対象メソッドを叩いて、結果を確認します。
基本的に1メソッドに対して1テストケース作ります。正常/異常などのパターンが有る場合は、それぞれテストケースを作ります。

おわりに

モックを使ったテストについて、ここ数ヶ月の経験を元にまとめました:ok_hand:
私はプロダクトコードを書いてからテストコードを書くことが多いですが、そうすると結構なことプロダクトコードを修正しなければならず面倒です:weary:
もっとテストを意識するか、テストを先に書く必要性を感じます:octopus:
モックを書くのもなかなか面倒ですよね:new_moon_with_face:
Androidにはmockitoなる便利なライブラリがあるらしく、羨ましい限りです:star2:

11
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?