Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Swiftユニットテスト入門

 Swiftユニットテスト入門

勉強会での発表資料。
サンプルコードはこちら。
https://github.com/yopicpic/UnitTestSample

Yoshiyuki Tanaka

January 26, 2018
Tweet

Other Decks in Programming

Transcript

  1. 自己紹介 All rights reserved visasQ inc. • 田中 慶之(Yoshiyuki Tanaka)

    • @yopicpic • 株式会社ビザスク(2017/4〜) • iOSエンジニア • Webマッチングサービス「ビザスク」のプロダクトオーナー
  2. アジェンダ All rights reserved visasQ inc. • Xcodeでユニットテスト ◦ テストの作成・実行

    • テストがしやすいアプリの設計 ◦ アーキテクチャ ◦ DI • 実践的なユニットテスト ◦ MVP ◦ APIKit ◦ Realm • まとめ
  3. ユニットテストとは All rights reserved visasQ inc. • メソッド単位のテスト • 効果

    ◦ 不具合の発見 ◦ リファクタリングを支える ◦ 設計品質の向上
  4. XCTest All rights reserved visasQ inc. • iOSのテストフレームワーク ◦ Xcode5から導入

    • パフォーマンステスト、UIテストも可能 • テストカバレッジの計測も可能
  5. テスト対象:サンプルコード All rights reserved visasQ inc. class Calculation { //

    引数を足して返す func sum(_ a: Int, _ b: Int) -> Int { return a + b } // 引数の平均を返す func avg(_ a: Int, _ b: Int) -> Float { return Float(a + b) / 2 } }
  6. テストコード All rights reserved visasQ inc. func testSum() { let

    target = Calculation() let result = target.sum(1, 2) XCTAssertEqual(result, 3) } func testAvg() { let target = Calculation() let result = target.avg(1, 2) XCTAssertEqual(result, 1.5) }
  7. Assertメソッド All rights reserved visasQ inc. • XCTAssert • XCTAssertEqual

    • XCTAssertNotEqual • XCTAssertTrue • XCTAssertFalse • XCTAssertNil • XCTAssertNotNil • XCTAssertThrowError • XCTAssertNoThrow • XCTAssertLessThan • XCTAssertLessThanOrEqual • XCTAssertGreatherThan • XCTAssertGreatherThanOrEqual
  8. FatViewController問題 All rights reserved visasQ inc. • ViewControllerの責務が肥大化していく ◦ ライフサイクルの管理

    ◦ 表示の管理 ◦ 表示ロジックの管理 ◦ 画面遷移の管理 ◦ 通信処理 ◦ DBの保存処理 メンテナンス性の低下・テストが書きにくくなる
  9. iOSアプリのアーキテクチャ All rights reserved visasQ inc. • MVC ◦ 普通に作るとこれになる

    • MVP ◦ 表示処理と表示ロジックを分離できるのでオススメ • MVVM ◦ RxSwift等を導入するコストが高い • Clean Architecture ◦ コード量が多くなるのでアプリの規模次第 • VIPER ◦ コード量が多くなるのでアプリの規模次第
  10. ビザスクの例 All rights reserved visasQ inc. Presentation Domain Data View

    ViewModel Service Entity Repository DataStore Data MVVM + Clean Architecture ライフサイクルの管理 表示の管理 表示ロジックの管理 画面遷移の管理 通信処理 DBの保存処理 View以外はテスト対象
  11. Dependency Injection All rights reserved visasQ inc. • DI(依存性の注入)とは ◦

    依存関係を外部から注入するパターン ◦ テスト時に特定のオブジェクトをモックオブジェクトに差し替えること ができる ◦ イニシャライザ,プロパティ,メソッドで注入する方法がある ◦ Swinject, Cleanse,DIKitなどのライブラリがある
  12. Dependency Injectionの例 All rights reserved visasQ inc. protocol UserServiceProtocol {

    func getUserName(by userID: String) -> String } class UserService: UserServiceProtocol { func getUserName(by userID: String) -> String { // APIやDBからユーザー名を取得する処理 } } // イニシャライザで注入 class UserPageViewModel { let userService: UserServiceProtocol init(userService: UserServiceProtocol ) { self.userService = userService // DIを使わない場合 // self.userService = UserService() } }
  13. Dependency Injectionの例2 All rights reserved visasQ inc. // プロパティで注入 class

    UserPageViewModel { var userService: UserServiceProtocol? } // メソッドで注入 class UserPageViewModel { private var userService: UserServiceProtocol ? func inject(with userService: UserServiceProtocol ) { self.userService = userService } }
  14. よくあるアプリの表示フロー All rights reserved visasQ inc. 表示開始 キャッシュの 有無 読み込みの

    成否 有 否 無 成 読み込み中 読み込み失敗 読み込み完了 キャッシュ表示
  15. よくあるアプリの実装方針 • アーキテクチャ ◦ MVP • API通信(JSON) ◦ APIKit •

    JSONのオブジェクト化 ◦ Decodable • キャッシュ保存 ◦ Realm
  16. よくあるアプリの設計 All rights reserved visasQ inc. View Presenter Service Repository

    APIKitを使ってユー ザー情報(JSON)を取 得してRealm Object に変換 ユーザー情報 (Realm Object)を保存 Service経由でユー ザー情報を取得。 Viewの表示処理を呼 び出す Presenterの命令に従 い、表示を変更する API Client Data
  17. ViewのProtocolの実装 All rights reserved visasQ inc. protocol UserPageView: class {

    func showIcon(url: String?) func showName(name: String) func showIntroduction(introduction: String) func showErrorView() func showIndicator() func hideErrorView() func hideIndicator() } class UserPageViewController: UIViewController, UserPageView { @IBOutlet weak var name: UILabel! private var presenter: UserPagePresenter! override func viewDidload() { presenter = UserPagePresenter(view: self) presenter.show() } func showName(name: String) { self.name.text = name } }
  18. Presenterの実装 All rights reserved visasQ inc. class UserPagePresenter { private

    weak var view: UserPageView? private let userService: UserServiceProtocol init(view: UserPageView, userService: UserServiceProtocol = UserService()) { self.view = view self.userService = userService } func show() { // キャッシュ読み込み処理は省略。通信で取得するパターン view?.showIndicator() userService.getProfile { [weak self] result in switch result { case .success(let user): self?.view?.hideIndicator() self?.view?.showIcon(url: user.imageURL) self?.view?.showName(name: user.name) self?.view?.showIntroduction(introduction: user.introduction) case .failure: self?.view?.hideIndicator() self?.view?.showErrorView() } } } }
  19. Presenterのテスト準備(ServiceのMockの用意) All rights reserved visasQ inc. protocol UserServiceProtocol { func

    getProfile(_ handler: @escaping (Result<User, SessionTaskError>) -> Void) func getCacheProfile() -> User?) } class MockUserService: UserServiceProtocol { var profileResult: Result<User, SessionTaskError>! var cacheProfile: User? = nil func getProfile(_ handler: @escaping (Result<User, SessionTaskError>) -> Void) { handler(profileResult) } func getCacheProfile() -> User? { return profile } }
  20. Presenterのテスト準備(ViewのMockの用意) All rights reserved visasQ inc. class MockUserPageView: UserPageView {

    var iconURL: String? = nil var name = "" var introduction = "" var isErrorViewShown = false var isIndicatorShown = false var isErrorViewHidden = false var isIndicatorHidden = false func showIcon(url: String?) { iconURL = url } func showName(name: String) { self.name = name } func showIntroduction(introduction: String) { self.introduction = introduction } func showErrorView() { isErrorViewShown = true } func showIndicator() { isIndicatorShown = true } func hideErrorView() { isErrorViewHidden = true } func hideIndicator() { isIndicatorHidden = true } }
  21. Presenterのテスト All rights reserved visasQ inc. class UserPagePresenterTests: XCTestCase {

    func test正常にプロフィール取得() { // セットアップ let mockUserPageView = MockUserPageView() let mockUserService = MockUserService() let user = User() user.name = "田中" user.imageURL = "http://dummy.co.jp/a.png" user.introduction = "こんにちは" mockUserService.profileResult = .success(user) let presenter = UserPagePresenter(view: mockUserPageView, userService: mockUserService) // 実行 presenter.show() // 評価 XCTAssertTrue(mockUserPageView.isIndicatorShown) XCTAssertEqual(mockUserPageView.name, "田中") XCTAssertEqual(mockUserPageView.iconURL, "http://dummy.co.jp/a.png") XCTAssertEqual(mockUserPageView.introduction, "こんにちは") XCTAssertTrue(mockUserPageView.isIndicatorHidden) } }
  22. API Clientの実装 All rights reserved visasQ inc. struct UserAPI {

    struct GetMyProfile: BaseRequest { typealias Response = User var method: HTTPMethod { return .get } var path: String { return "/profile" } var parameters: Any? { return nil } // レスポンスをData型で返す必要がある // https://qiita.com/sgr-ksmt/items/e822a379d41462e05e0d var dataParser: DataParser { return DecodableDataParser () } func response(from object: Any, urlResponse: HTTPURLResponse ) throws -> Response { guard let data = object as? Data else { /* エラーを返す */ } return try JSONDecoder().decode(User.self, from: data) } } } class DecodableDataParser: DataParser { var contentType: String? { return "application/json" } func parse(data: Data) throws -> Any { return data } }
  23. Userの実装 All rights reserved visasQ inc. @objcMembers class User: Object,

    Decodable { enum CodingKeys: String, CodingKey { case id case name = "display_name" case imageURL = "image_url" case introduction = "description" } @objc dynamic var id: Int = 0 dynamic var name = "" dynamic var imageURL: String? dynamic var introduction = "" override static func primaryKey() -> String? { return "id" } }
  24. API Clientのテスト All rights reserved visasQ inc. override func setUp()

    { _ = stub(condition: isPath("/profile")) { _ in let stubPath = OHPathForFile("profile.json", type(of: self)) return fixture(filePath: stubPath!, headers: nil) } } override func tearDown() { OHHTTPStubs.removeAllStubs() } func testプロフィール取得の成功() { let expectation = XCTestExpectation(description: "get my profile") Session.send(UserAPI.GetMyProfile()) { result in switch result { case .success(let response): XCTAssertEqual(response.name, "田中") XCTAssertEqual(response.introduction, "こんにちは") XCTAssertEqual(response.imageURL, "http://dummy.co.jp/a.png") expectation.fulfill() case .failure: XCTFail() } } wait(for: [expectation], timeout: 10.0) }
  25. Repositoryの実装 All rights reserved visasQ inc. protocol UserRepositoryProtocol { func

    getMyProfile() -> User? func createOrUpdateMyProfile( _ user: User) } class UserRepository: UserRepositoryProtocol { func getMyProfile() -> User? { return try! Realm().objects(User.self).first } func createOrUpdateMyProfile( _ user: User) { let realm = try! Realm() try! realm.write { realm.add(user, update: true) } } }
  26. Repositoryのテスト All rights reserved visasQ inc. class UserRepositoryTests: XCTestCase {

    override func setUp() { Realm.Configuration.defaultConfiguration .inMemoryIdentifier = self.name } func tests保存と取得() { // セットアップ let user = User() user.id = 100 let repository = UserRepository() // 実行 repository.createOrUpdateMyProfile (user) // 結果 let result = repository. getMyProfile() XCTAssertEqual(result?.id, 100) } }
  27. 参考: Serviceの実装 All rights reserved visasQ inc. class UserService: UserServiceProtocol

    { private let userRepository: UserRepositoryProtocol init(userRepository: UserRepositoryProtocol = UserRepository()){ self.userRepository = userRepository } func getCacheProfile() -> User? { return userRepository.getMyProfile() } func getProfile(_ handler: @escaping (Result<User, SessionTaskError>) -> Void) { let request = UserAPI.GetMyProfile() Session.shared.send(request) { [weak self] result in switch result { case .success(let user): self?.userRepository.createOrUpdateMyProfile(user) handler(Result.success(user)) case .failure(let error): handler(Result.failure(error) } } } }
  28. 最後に All rights reserved visasQ inc. • 言語 ◦ Swift4

    • アーキテクチャ ◦ MVVM • ライブラリ ◦ RxSwift, APIKit, Himotoki, Quck, etc... • CI環境 ◦ Fastlane, Bitrise, Slack, API Gatway, Lambda • 今後やりたいこと ◦ 通話機能、ビデオチャット機能、グローバル対応, etc... 2人目のiOSエンジニア募集中! Webエンジニア(Python)も募集中!