43
33

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アプリでMockを使ってUnitTestを書く

Last updated at Posted at 2017-07-28

VIPERやClean Architectureなどでは当然されていますが、UTをするためにはProtocolを用いて各レイヤー間の依存度を下げテストをしやすくする必要があります。

##テストしにくい(依存度が高い)コード

class BookmarkViewModel {
    let model = BookmarkModel()
    var bookmarks = [Bookmark]()
    
    // DBからブックマーク一覧を取得
    func loadBookmarkList() {
        bookmarks = model.load()
    }

    // DBからi番目のブックマークを削除
    func deleteBookmark(at index: Int) {
        model.delete(at: index)
    }
}

このコードではBookmarkViewModelBookmarkModelに依存しているため、
BookmarkViewModelのテストをするときは以下のようにViewModelの範囲を超えてModelレイヤーのことまで意識しないといけなくなってしまいます。

class BookmarkViewModelTests: XCTestCase {
    var vm = BookmarkViewModel()
    func testLoadBookmarkList() {
        let model = BookmarkModel()
        // 例:BookmarkModelが操作するDBを意識してテスト用に2件データを追加
        model.insert(Bookmark(id: "bookmark1"))
        model.insert(Bookmark(id: "bookmark2"))

        vm.loadBookmarkList()
        // 上で2件追加したので取得も2件でbookmarks変数に2件あるはず
        XCTAssertEqual(vm.bookmarks.count, 2)
    }
}

このような単純なModelならそこまでテストに苦労はしませんが、複雑になってくるにつれテストが行いにくくなってきます。そのような状況になってもテストをするために、Protocolを用いてViewModelからModelの実態を意識させないようにします。

##テストしやすい(依存度が低い)コード

protocol BookmarkModelProtocol {
    func load() -> [Bookmark]
    func delete(at index: Int)
}

class BookmarkModel: BookmarkModelProtocol {
    // 本来の実装すべきロジック
}

class BookmarkViewModel {
    let model: BookmarkModelProtocol
    var bookmarks = [Bookmark]()

    init(model: BookmarkModelProtocol = BookmarkModel()) {
       self.model = model
    }
    
    func loadBookmarkList() {
        bookmarks = model.load()
    }

    func deleteBookmark(at index: Int) {
        model.delete(at: index)
    }
}

このコードになるとBookmarkViewModelBookmarkModelProtocolにしか依存してなくinit経由で自由にオブジェクトを差し替えることができます。
そのためテストでは以下のように通常の時とは違う挙動できるようになります。

class BookmarkModelMock: BookmarkModelProtocol {
    func load() -> [Bookmark] {
        // 2件返す
        return [Bookmark(id: "bookmark1"), Bookmark(id: "bookmark2")]
    }

    func delete(at index: Int) { // なにもしない }
}

class BookmarkViewModelTests: XCTestCase {
    var vm = BookmarkViewModel(model: BookmarkModelMock())
    func testLoadBookmarkList() {
        vm.loadBookmarkList()
        // mockでは2件返すので、それをちゃんと変数に入れてるかのチェックだけ気にする
        XCTAssertEqual(vm.bookmarks.count, 2)
    }
}

このようにMockを使うことで他レイヤーのことを気にせずTest対象クラスに閉じた挙動をテストできます。
また他レイヤーに影響を与えないので、最後にDBの全削除などの後処理も不要になり影響範囲が小さくなります。

ただ現状のMockだと固定値を返すだけで、返却値がないメソッドは動作の確認をできないので、以下のように変更を加えテストできるようにします。


// インスタンス変数を保持するのでテストケース毎の初期化が必要
class BookmarkModelMock: BookmarkModelProtocol {
    var isLoadCalled = false
    var loadResult = [Bookmark]()
    func load() -> [Bookmark] {
        isLoadCalled = true
        return loadResult
    }

    var isDeleteCalled = false
    var deleteParam: Int?
    func delete(at index: Int) { 
        isDeleteCalled = true
        deleteParam = index
    }
}

class BookmarkViewModelTests: XCTestCase {
    func testLoadBookmarkList() {
        let mock = BookmarkModelMock()
        // テストパターンに応じて外部から返却値を設定(今回は2件返す)
        mock.loadResult = [Bookmark(id: "bookmark1"), Bookmark(id: "bookmark2")]

        let vm = BookmarkViewModel(model: mock)
        vm.loadBookmarkList()
        // mockでは2件返すので、それをちゃんと変数に入れてるかチェック
        XCTAssertEqual(vm.bookmarks.count, 2)

        mock.loadResult.removeAll()
        vm.loadBookmarkList()
        // modelの値が変わったらちゃんと反映されるかチェック
        XCTAssertEqual(vm.bookmarks.count, 0)
    }

    func testDelete() {
        let mock = BookmarkModelMock()
        let vm = BookmarkViewModel(model: mock)
        // まだ呼ばれないのでfalse
        XCTAssertFalse(mock.isDeleteCalled)

        vm.remove(at: 0)
        // BookmarkViewModelから正しくModelのメソッドが呼ばれているかチェック
        XCTAssertTrue(mock.isDeleteCalled)
        XCTAssertEqual(mock.deleteParam!, 0)
    }
}

このようにすることでレイヤーまたぎの処理なども影響範囲を小さくしつつ、正しい振る舞いができているかテストすることができます。

ちなみにAndroidでは、このようなことができるmockitoというライブラリがありますが、iOSではなかったのでこの様な形で実装しました。

43
33
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
43
33

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?