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

Flux + Repositoryへリアーキテクチャ後にテストを書き始めて1ヶ月経った話

Flux + Repositoryへリアーキテクチャ後にテストを書き始めて1ヶ月経った話

yuki anzai

July 25, 2019
Tweet

More Decks by yuki anzai

Other Decks in Programming

Transcript

  1. ࣗݾ঺հ  ҆ᜊ༞لʢ͋Μ͍͟Ώ͏͖ʣ Twitter: @off2white connpass: tonyu 株式会社 ディー・エヌ・エー -

    次世代タクシー配⾞サービス「MOV」 - Androidアプリ開発担当 - プロジェクト管理とコーディングの割合 = 50:50 (気持ちは) 最近困っていること Kotlin-festのLTに採択されたはいいものの いまだに運営さんが⼈を間違えていないか⼼配 Twitter connpass
  2. ΞʔΩςΫνϟʢ'MVY 3FQPTJUPSZ 4UBUF.BDIJOF  'SBHNFOU "DUJPO $SFBUPS %JTQBUDIFS 4UPSF 3FQPTJUPS

    Z "1* %# 1SFG "DUJPO "DUJPO 4UBUF .BDIJOF "DUJPO "DUJWJUZ /BWJHBUJPO (MPCBM"DUJPO '$. .FUFS ֎෦ 3Y
  3. ϲ݄લͷ͓ؾ࣋ͪද໌ • 6OJU5FTUT • +6OJU • *OUFHSBUJPO5FTUT • 4QFLͰهࡌ •

    ࢓༷Λग़ྗ͍ͨ͠ͳ͊ • &F5FTUT • มΘΒͳͦ͏ͳͱ͜Ζ͚࣮ͩ૷ • ۀ຿༻ΞϓϦͳͷͰෳ਺σΟεϓϨΠαΠζΛߟྀ͢Δඞཁ͕ͳ͍ͷͰ΍Γ΍ͦ͢͏ɻ ಉ࣌ʹεϓϦϯτຖʹ2"νʔϜʹखಈςετΛ͓ئ͍͍ͯ͘͠ӡ༻  ։ൃऴ͔ྃ࣌ΒϦϦʔε·ͰͷظؒΛͳΔ΂͘ݮΒ͢໨త  https://codelabs.developers.google.com/codelabs/android-testing/#2
  4. ΞʔΩςΫνϟʢ'MVY 3FQPTJUPSZ 4UBUF.BDIJOF  'SBHNFOU "DUJPO $SFBUPS %JTQBUDIFS 4UPSF 3FQPTJUPS

    Z "1* %# 1SFG "DUJPO "DUJPO 4UBUF .BDIJOF "DUJPO "DUJWJUZ /BWJHBUJPO (MPCBM"DUJPO '$. .FUFS ֎෦ 3Y Unit Tests Integration Tests
  5. ϲ݄ޙͷ͓ؾ࣋ͪ • 6OJU5FTUT • +6OJU • *OUFHSBUJPO5FTUT • 'MVYपΓ͚ͩ4QFLͰهࡌ •

    ࢓༷Λग़ྗ͍ͨ͠ • &F5FTUT • 6*͕೔࣍ͰมΘ͍ͬͯ͘ͷͰແཧʢ஌ͬͯͨʣ • มΘΒͳͦ͏ͳͱ͜Ζ͚࣮ͩ૷ • ۀ຿༻ΞϓϦͳͷͰෳ਺σΟεϓϨΠαΠζΛߟྀ͢Δඞཁ͕ͳ͍ͷͰ΍Γ΍ͦ͢͏ɻ ಉ࣌ʹεϓϦϯτຖʹ2"νʔϜʹखಈςετΛ͓ئ͍͍ͯ͘͠ӡ༻  ։ൃऴ͔ྃ࣌ΒϦϦʔε·ͰͷظؒΛͳΔ΂͘ݮΒ͢໨త  https://codelabs.developers.google.com/codelabs/android-testing/#2
  6. 6OJUUFTUͰॻ͔ͳ͔ͬͨͱ͜Ζ3FQPTJUPSZ  Fragment Action Creator Dis patcher Store Repository API

    DB Pref Action Action State Machine (App Store) Action Activity Navigation (Global Action) verify(exactly = 0) { apiA.request (… verify(exactly = 0) { apiB.request (… coVerify(exactly = 1) { database.save(… 書いてはみたものの 「mockメソッドが 順番通り呼ばれているか」 くらいの確認しかできなかったので 実りが少なそう → Integration Testsで担保する
  7. 6OJUUFTUͰॻ͔ͳ͔ͬͨͱ͜Ζ"DUJPO$SFBUPS 4UPSF  'SBHNFOU "DUJPO $SFBUPS %JTQBUDIFS 4UPSF 3FQPTJUPSZ "1*

    %# 1SFG Action $PNQMFUFE "DUJPO 4UBUF .BDIJOF "QQ4UPSF Action "DUJWJUZ Navigation (Global Action) Read only ActionCreatorがActionを発⾏しているかどうか 確認するためにdispatcherを監視する必要があり ならもういっそIntegration Testsで良いかな、 という気持ち val store: Store by memoized { Store(dispatcher) } val statusObserver: Observer<Status> by memoized { // テスト対象 val actionCreator: ActionCreator by memoized { // Given store.status.observeForever(statusObserver)
  8.  object Test : Spek({ applyTestTaskExecutor() applyTestCoroutineDispather( Feature(”XXXΛঝ୚͢Δ“) { //

    Flux val dispatcher = Dispatcher(Schedulers.trampoline()) val store: Store by memoized {Store(dispatcher) } val statusObserver: Observer<Status> by memoized { spyk () } // ςετର৅ val actionCreator: ActionCreator by memoized { DispatchOrderActionCreator( dispatcher = dispatcher, aRepository = mockk(relaxed = true) )
  9.  Scenario(”XXXϘλϯ͕ԡ͞Εͨ৔߹ɺXXX͕௨஌͞ΕΔ“) { Given(”εςʔλεΛ؂ࢹ͢Δ“) { store.status.observeForever(statusObserver) } When(”XXXϘλϯ͕ԡ͞Εͨ“) { runBlocking

    { actionCreator.consentCarRequest() } } Then(“XXX͕௨஌͞ΕΔ") { verifySequence { statusObserver.onChanged(DispatchOrderStatus.InProgress) statusObserver.onChanged(DispatchOrderStatus.OnCompleted) } }
  10. *OGSBSFQPTJUPSZJNQM 3FQPTJUPSZ$PNQPOFOU 3FQPTJUPSZ$PNQPOFOU.PEVMF  *OUFSOBM 3FQPTJUPSZ.PEVMF 3FQPTJUPSZ*NQM *OGSBECJNQM %#$PNQPOFOU %#$PNQPOFOU.PEVMF

    *OUFSOBM %BHHFS.PEVMF EC*NQM BQQ %BHHFS$PNQPOFOU ֤ϞδϡʔϧͰ͸ $PNQPOFOUͱ $PNQPOFOU.PEVMFͷΈ ެ։͍ͯ͠Δ
  11.  %C.PEVMF %C$PNQPOFOU %C$PNQPOFOU.PEVMF @Module(includes = [DbModule.Providers::class]) internal abstract class

    DbModule { @Binds abstract fun requestDatabase(impl: requestDatabaseImpl): RequestDatabase @Module internal object Providers { @JvmStatic @Provides fun requestDao(db: requestRoomDb): RequestDao { return db.requestDao() }
  12.  %C.PEVMF %C$PNQPOFOU %C$PNQPOFOU.PEVMF @Singleton @Component( modules = [ DbModule::class

    ] ) interface DbComponent { fun requestDatabase(): RequestDatabase
  13.  %C.PEVMF %C$PNQPOFOU %C$PNQPOFOU.PEVMF @Module object DbComponentModule { @JvmStatic @Provides

    @AppScope fun provideRequestDatabase( component: DbComponent ): RequestDatabase { return component.requestDatabase() } @JvmStatic @Provides @AppScope fun provideDbComponent( context: Context, gson: Gson ): DbComponent {
  14. *OGSBSFQPTJUPSZJNQM 3FQPTJUPSZ$PNQPOFOU 3FQPTJUPSZ$PNQPOFOU.PEVMF  *OUFSOBM 3FQPTJUPSZ.PEVMF 3FQPTJUPSZ*NQM *OGSBECJNQM %#$PNQPOFOU %#$PNQPOFOU.PEVMF

    *OUFSOBM %BHHFS.PEVMF EC*NQM BQQ %BHHFS$PNQPOFOU BQQϞδϡʔϧͰ͸ ֤Ϟδϡʔϧͷ $PNQPOFOU.PEVMFΛ .PEVMFͱͯ͠ઃఆ͠ ґଘੑΛղܾ͍ͯ͠Δ
  15.  $PNQPOFOUͷ#VJMEFS͔ΒΠϯελϯε Λੜ੒ interface RepositoryComponent { fun RequestRepository(): RequestRepository @Component.Builder

    interface Builder { @BindsInstance fun api(api: Api): Builder @BindsInstance fun gson(gson: Gson): Builder fun build(): RepositoryComponent } companion object { fun builder(): Builder = RepositoryComponent.builder() } }
  16.  ͜ΕͰ*NQM͕औಘͰ͖Δʂ @Before fun setup() { repository = MovApiRepositoryComponent.builder() .api(mockk(relaxed

    = true)) .gson(mockk(relaxed = true)) .requestDatabase(mockk(relaxed = true)) .movApiRequestDatabase(mockk(relaxed = true)) .taxiBusinessRepository(mockk(relaxed = true)) .build() .MovCarRequestRepository() } 3FQPTJUPSZͷ*NQM͕ੜ੒Ͱ͖ͨʂ
  17.  @Component.Builder interface Builder { @BindsInstance fun context(context: Context): Builder

    @BindsInstance fun coroutineContext(coroutineContext: CoroutineContext): Builder @BindsInstance fun gson(gson: Gson): Builder @BindsInstance fun testFlag(isTest: Boolean = false): Builder // BuilderʹtestFlagΛ࡞੒ fun build(): DbComponent } %C$PNQPOFOULU
  18.  @Module internal object Providers { @Singleton @JvmStatic @Provides fun

    requestRoomDb( context: Context, isTest: Boolean ): requestRoomDb { // ϑϥάʹΑͬͯ෼ذ return if (isTest) { Room.inMemoryDatabaseBuilder(context, RequestRoomDb::class.java).build() } else { Room.databaseBuilder( context, RequestRoomDb::class.java, "request.db" ).build() } %C.PEVMFLU
  19.  W[ @Before fun setup() { val context = ApplicationProvider.getApplicationContext<Context>()

    component = DbComponent.builder() .context(context) .coroutineContext(Dispatchers.IO) .gson(mockk(relaxed = true)) .testFlag(true) .build() repository = MovApiRepositoryComponent.builder() .api(mockk(relaxed = true)) .gson(mockk(relaxed = true)) .requestDatabase(component.movCarRequestDatabase()) .build() .requestRepository() } 3FQPTJUPSZ5FTULU
  20.  internal class DebugMovApiImpl @Inject constructor( private val movApi: MovApi

    ) : DebugMovApi { override fun getXXXRequest( id: Long ): Single<XXXRequestResponse> = if (useMockResponse) { Single.just(XXXRequestResponse()) } else { movApi.getXXXRequest(id) } } %FCVH"QJ*NQMLU
  21.  ςετ༻ͷϨεϙϯεσʔλ͕ฦͤΔʂ val movApi = DebugApiComponent.builder() .movApi(mockk(relaxed = true)) .build()

    .debugApi() .apply { useMockResponse = true } 3FQPTJUPSZ5FTULU ςετ༻ͷϨεϙϯεσʔλ͕ฦͤΔʂ
  22. ࣍ͷνϟϨϯδ • 'MVYͷςετʹؔͯ͠͸'SBHNFOU͔ΒςετͰ͖ͨΒྑͦ͞͏ • 4QFLΛར༻͢Δ͔Ͳ͏͔೰Ήʜ • ͋Δఔ౓ςετ࣮૷ํ๏͕ݻ·͖ͬͯͨͷͰɺϓϩδΣΫτϝϯόʔͰڞ༗ • ͪΒ΄ΒΞʔΩςΫνϟʹԊ͍ͬͯͳ͍࣮૷͕ग़͖͍ͯͯΔͷͰɺςετ͕ॻ͚ͳ͍࣮ ૷ʹͳ͍ͬͯͨΒ௚ͯ͠΋Β͏

    • εϓϦϯτ2"࣌ͷෆ۩߹݅਺ͷ௿ԼΛ௥ͬͯΈΔ • 2"޻਺࡟ݮΛૂͬͯΑΓεϐʔυͷ͋Δ։ൃΛ໨ࢦ͢ • ը໘͕ݻ·͖ͬͯͨΒ6*ςετΛ໨ࢦ͢ • ·ͩఘΊͯͳ͍ • ಛʹύλʔϯʹΑͬͯ6*͕੾ΓସΘΔͱ͜Ζ͋ͨΓॻ͖͍ͨ