単体テストを書こうと思った時、テスト対象のクラスが外部クラスに依存していると非常にテストしにくいです
そんな時は、外部クラスをモックに置き換えてしまうとテスト対象クラスだけをテストすることができます
テスト対象クラス
この記事では、外部依存クラスとしてUIAlertController
とNotificationCenter
を含む以下のクラスをテスト対象とします。ボタンが押されたら通知を飛ばすorアラートを表示する非常にシンプルなクラスです。
UIAlertController
にはshowOkAlert()
という拡張メソッドをもたせます。
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)
}
}
モックを作る
早速UIAlertController
とNotificationCenter
のモックを作ります。
モックの作り方は2つあります。
プロトコルで作る
元のクラスとモッククラスが持つべきインターフェースをまとめてプロトコルにし、両方に実装させます。
このやり方ならどんなクラスにも適用できるので、基本的にはこちらの方法がオススメです。
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
を例にすると以下の通りです。
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
なりを直接使ってしまっているので、これらをプロパティにします。
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
とすることでプロパティとして持つことができます。わざわざシングルトンにしなくても大丈夫です。
もしプロパティを公開することに抵抗があるなら、セッターを作ってもいいかもしれません。
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")
}
}
イニシャライザ的なメソッドを作り、その引数で渡すというのも良いです。
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を採用したとして記事を進めます。
モックを使ってテストする
それではいよいよテストを書きましょう。
テストではViewController
のnotify()
と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テストケース作ります。正常/異常などのパターンが有る場合は、それぞれテストケースを作ります。
おわりに
モックを使ったテストについて、ここ数ヶ月の経験を元にまとめました
私はプロダクトコードを書いてからテストコードを書くことが多いですが、そうすると結構なことプロダクトコードを修正しなければならず面倒です
もっとテストを意識するか、テストを先に書く必要性を感じます
モックを書くのもなかなか面倒ですよね
Androidにはmockitoなる便利なライブラリがあるらしく、羨ましい限りです