Slide 1

Slide 1 text

【TCA】書きやすくて分かりやすい! Reducerのテストの基本 Sansan株式会社 技術本部 Seminar One Engineeringグループ 池端 貴恵 A f t e r i O S D C J a p a n 2 0 2 1

Slide 2

Slide 2 text

Sansan Seminar Managerを作っています。 ちょっと前は、EightのiOSアプリをつくってました。 池端 貴恵 Sansan株式会社 技術本部 Seminar One Engineeringグループ 写真

Slide 3

Slide 3 text

Agenda - The Composable Architectureとは - テストしたいこと - 同期的な処理のテスト - ⾮同期的な処理のテスト - テスト失敗 - まとめ

Slide 4

Slide 4 text

The Composable Architectureとは

Slide 5

Slide 5 text

The Composable Architecture → The Composable Architectureは、コンポジション、テスト、エルゴノミクス(※ハードウェアやソフ トウェアなどを、快適で使いやすい道具にするための設計・デザイン)を考慮した、⼀貫性のある理解 しやすい⽅法でアプリケーションを構築するためのライブラリです https://github.com/pointfreeco/swift-composable-architecture

Slide 6

Slide 6 text

The Composable Architecture https://medium.com/swlh/the-composable-architecture-visualize-data-flows-with-a-diagram-817306831508

Slide 7

Slide 7 text

The Composable Architecture https://medium.com/swlh/the-composable-architecture-visualize-data-flows-with-a-diagram-817306831508 enum Action: Equatable { case buttonTapped }

Slide 8

Slide 8 text

The Composable Architecture https://medium.com/swlh/the-composable-architecture-visualize-data-flows-with-a-diagram-817306831508 enum Action: Equatable { case buttonTapped } let reducer = Reducer { state, action, environment in switch action { case .buttonTapped: state.count += 1 return .none } }

Slide 9

Slide 9 text

The Composable Architecture https://medium.com/swlh/the-composable-architecture-visualize-data-flows-with-a-diagram-817306831508 let reducer = Reducer { state, action, environment in switch action { case .buttonTapped: count += 1 return .none } } struct Action: Equatable { var count = 0 }

Slide 10

Slide 10 text

The Composable Architecture https://medium.com/swlh/the-composable-architecture-visualize-data-flows-with-a-diagram-817306831508 struct Action: Equatable { var count = 0

Slide 11

Slide 11 text

The Composable Architecture https://medium.com/swlh/the-composable-architecture-visualize-data-flows-with-a-diagram-817306831508 let reducer = Reducer { state, action, environment in switch action { case .get: state.count += 1 // var get: () -> Effect return environment.hogeClient.get() .receive(on: environment.mainQueue) .catchToEffect() .map(Action.nextAction)

Slide 12

Slide 12 text

The Composable Architecture PointFree公式解説 iOSアプリ開発のための “The Composable Architecture” がすごく良いので紹介したい

Slide 13

Slide 13 text

テストしたいこと

Slide 14

Slide 14 text

TCAで⽤意されているテストについて How to not only test a feature built in the architecture, but also write integration tests for features that have been composed of many parts, and write end-to-end tests to understand how side effects influence your application. This allows you to make strong guarantees that your business logic is running in the way you expect. https://github.com/pointfreeco/swift-composable-architecture 基本的なテストの⽅針: ReducerのStateの変更ロジックが正しく実⾏されているかテストする

Slide 15

Slide 15 text

テストする対象 サインイン画⾯ 要件 - メアドやパスワードが⼊⼒できること - ⼊⼒された内容でログインできること

Slide 16

Slide 16 text

テスト対象のコード struct LoginState: Equatable { var email = "" var password = "" var loginSucceeded = false } enum LoginAction: Equatable { case emailChanged(String) case passwordChanged(String) case loginButtonTapped case login(Result) } struct LoginEnvironment { let loginClient: LoginClient let mainQueue: AnySchedulerOf }

Slide 17

Slide 17 text

テスト対象のコード struct LoginView: View { let store: Store var body: some View { WithViewStore(self.store) { viewStore in VStack(alignment: .center, spacing: 5) { TextField("MailAddress", text: viewStore.binding( get: { $0.email }, send: LoginAction.emailChanged ) ).textFieldStyle(RoundedBorderTextFieldStyle()) .padding() } .... 初期化 LoginView(store: Store(initialState: LoginState(), reducer: loginReducer, environment: LoginEnvironment(省略))

Slide 18

Slide 18 text

テスト対象のコード let loginReducer = Reducer { state, action, environment in switch action { case let .emailChanged(email): state.email = email return .non case let .passwordChanged(password): state.password = password return .none case .loginButtonTapped: // var login: (Email, Password) -> Effect return environment.loginClient.login(state.email, state.password) .receive(on: environment.mainQueue) .catchToEffect() .map(LoginAction.login) case .login(.success): state.loginSucceeded = true return .none case .login(.failure): state.loginSucceeded = false return .none } }

Slide 19

Slide 19 text

テストすること 1. ⽂字⼊⼒されると、LoginStateのemailとpasswordが正しく更新されるか 2. LoginAPIの結果で、LoginStateのloginSucceededが正しく更新されるか

Slide 20

Slide 20 text

LoginStateのemailとpasswordに関するテスト (同期的な処理のテスト)

Slide 21

Slide 21 text

テストコード class loginTests: XCTestCase { let scheduler = DispatchQueue.test func test_⼊⼒値チェック() { let store = TestStore( initialState: LoginState(), reducer: loginReducer, environment: LoginEnvironment(loginClient: LoginClient(login:{ _, _ in fatalError()}), mainQueue: scheduler.eraseToAnyScheduler()) ) store.send(.emailChanged("deadbeef")) { // XCTestの XCTAssertEqual($0.email, “deadbeef”) みたいなことをしているのがココ↓ $0.email = “deadbeef” } store.send(.passwordChanged("deliciousbeef")) { $0.password = "deliciousbeef" } } }

Slide 22

Slide 22 text

LoginStateのloginSucceededに関するテスト (⾮同期的な処理のテスト)

Slide 23

Slide 23 text

class loginTests: XCTestCase { let scheduler = DispatchQueue.tes func test_ログイン成功() { let store = TestStore( initialState: LoginState(), reducer: loginReducer, environment: LoginEnvironment(loginClient: LoginClient(login: { _, _ in Effect(value: true)}), mainQueue: scheduler.eraseToAnyScheduler()) ) store.send(.loginButtonTapped) scheduler.advance() store.receive(.login(.success(true))) { $0.loginSucceeded = true } } テストコード Combineを使ったコードのテストを Schedulerで操る⽅法とその仕組み

Slide 24

Slide 24 text

テスト失敗😭

Slide 25

Slide 25 text

失敗パターン1 テストコード store.send(.emailChanged("deadbeef")) { $0.email = "deadbeef" }

Slide 26

Slide 26 text

失敗パターン1 実装 let loginReducer = Reducer { state, action, environment in switch action { case let .emailChanged(email): state.password = email <-- 😭 return .none

Slide 27

Slide 27 text

失敗パターン2 テストコード store.send(.loginButtonTapped) scheduler.advance() store.receive(.login(.success(true))) { $0.loginSucceeded = true }

Slide 28

Slide 28 text

失敗パターン2 実装 let loginReducer = Reducer { state, action, environment in switch action { case .loginButtonTapped: return environment.loginClient.login(state.email, state.password) .receive(on: environment.mainQueue) .catchToEffect() .map(LoginAction.hoge) <ー 😭

Slide 29

Slide 29 text

まとめ TCAにはロジック部分のテストを軽量にかける仕組みがある。 テストを書いて、安定した良いアプリを世に出そう。

Slide 30

Slide 30 text

29

Slide 31

Slide 31 text

全⽂書き起こしメディア Sansan – 働き⽅を変えるDX – ピアボーナスサービス 契約書データ化サービス スマート台帳 スマート判⼦ クラウド請求書受領サービス スマート受付 無⼈名刺受付システム クラウド名刺管理サービス イベント・セミナー 請求書 名刺 契約書 組織コミュニケーション 反社チェックオプション powered by Refinitiv/KYCC 契約管理オプション for クラウドサイン 商談管理オプション for Salesforce アンケートオプション powered by CREATIVE SURVEY powered by MotionBoard 名刺分析オプション 業務連携 名刺作成・発注 データ活⽤ スマートターゲット スマート名寄せ 新世代エントリーフォーム スマートエントリー 新世代パンフレット スマートパンフレット スマート署名取り込み AI名刺管理 オンライン名刺 スマート名刺メーカー スマートフォーム データ統合・活⽤サービス 法⼈向けセミナー管理システム 名刺作成サービス

Slide 32

Slide 32 text

We are hiring! 新規事業開発 PdM(プロダクトマネジャー) 新規事業開発エンジニア ウェブエンジニア サービス開発エンジニア 社内システム開発 コーポレートエンジニア セキュリティーエンジニア (SOC、社内教育・監査) 研究開発 ⾃然⾔語処理研究員 社会科学分野研究員 機械学習研究員 OCR開発技術者 インフラエンジニア R&D DevOpsエンジニア 研究開発エンジニア データエンジニア サービス基盤エンジニア データサイエンティスト サービス開発 SETエンジニア サービス開発エンジニア ウェブアプリケーションエンジニア インフラエンジニア iOSエンジニア Android エンジニア 募集中のポジション詳細はこちら https://jp.corp-sansan.com/recruit/midcareer

Slide 33

Slide 33 text

テックカンファレンス「Sansan Builders Stage」開催 Sansanのエンジニア情報サイト「Sansan Engineering」 プロダクト、テクノロジー、カルチャーや採⽤情報など、Sansan株式会社のエンジニアリングに関するあらゆる情報を掲載しています。 Sansan Engineering ENTRY HERE

Slide 34

Slide 34 text

※ 登録の際、紹介コードの欄に「1 0 0 6 i O S D C 」と⼊⼒ください。

Slide 35

Slide 35 text

No content