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

【TCA】書きやすくて分かりやすい!Reducerのテストの基本 / [TCA] the basics of testing in Reducer

Sansan
October 06, 2021

【TCA】書きやすくて分かりやすい!Reducerのテストの基本 / [TCA] the basics of testing in Reducer

■イベント

After iOSDC Japan 2021
https://zozotech-inc.connpass.com/event/222423/

■登壇概要

タイトル:【TCA】書きやすくて分かりやすい!Reducerのテストの基本

登壇者:技術本部 Seminar One Engineeringグループ 池端 貴恵

▼Sansan Engineering

https://jp.corp-sansan.com/engineering/

Sansan

October 06, 2021
Tweet

More Decks by Sansan

Other Decks in Technology

Transcript

  1. 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> { state, action, environment in switch action { case .buttonTapped: state.count += 1 return .none } }
  2. The Composable Architecture https://medium.com/swlh/the-composable-architecture-visualize-data-flows-with-a-diagram-817306831508 let reducer = Reducer<State, Action, Environment>

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

    { state, action, environment in switch action { case .get: state.count += 1 // var get: () -> Effect<Response, hogeClient.Failure> return environment.hogeClient.get() .receive(on: environment.mainQueue) .catchToEffect() .map(Action.nextAction)
  4. 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の変更ロジックが正しく実⾏されているかテストする
  5. テスト対象のコード struct LoginState: Equatable { var email = "" var

    password = "" var loginSucceeded = false } enum LoginAction: Equatable { case emailChanged(String) case passwordChanged(String) case loginButtonTapped case login(Result<Bool, LoginClient.Failure>) } struct LoginEnvironment { let loginClient: LoginClient let mainQueue: AnySchedulerOf<DispatchQueue> }
  6. テスト対象のコード struct LoginView: View { let store: Store<LoginState, LoginAction> 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(省略))
  7. テスト対象のコード let loginReducer = Reducer<LoginState, LoginAction, LoginEnvironment> { 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<Bool, LoginClient.Failure> 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 } }
  8. テストコード 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" } } }
  9. 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で操る⽅法とその仕組み
  10. 失敗パターン1 実装 let loginReducer = Reducer<LoginState, LoginAction, LoginEnvironment> { state,

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

    action, environment in switch action { case .loginButtonTapped: return environment.loginClient.login(state.email, state.password) .receive(on: environment.mainQueue) .catchToEffect() .map(LoginAction.hoge) <ー 😭
  12. 29

  13. 全⽂書き起こしメディア Sansan – 働き⽅を変えるDX – ピアボーナスサービス 契約書データ化サービス スマート台帳 スマート判⼦ クラウド請求書受領サービス

    スマート受付 無⼈名刺受付システム クラウド名刺管理サービス イベント・セミナー 請求書 名刺 契約書 組織コミュニケーション 反社チェックオプション powered by Refinitiv/KYCC 契約管理オプション for クラウドサイン 商談管理オプション for Salesforce アンケートオプション powered by CREATIVE SURVEY powered by MotionBoard 名刺分析オプション 業務連携 名刺作成・発注 データ活⽤ スマートターゲット スマート名寄せ 新世代エントリーフォーム スマートエントリー 新世代パンフレット スマートパンフレット スマート署名取り込み AI名刺管理 オンライン名刺 スマート名刺メーカー スマートフォーム データ統合・活⽤サービス 法⼈向けセミナー管理システム 名刺作成サービス
  14. We are hiring! 新規事業開発 PdM(プロダクトマネジャー) 新規事業開発エンジニア ウェブエンジニア サービス開発エンジニア 社内システム開発 コーポレートエンジニア

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