Share
Embed
Transcript
54 slides
-
ࣗݾհ ҆ᜊ༞لʢ͋Μ͍͟Ώ͏͖ʣ Twitter: @off2white connpass: tonyu 株式会社 ディー・エヌ・エー -
次世代タクシー配⾞サービス「MOV」 - Androidアプリ開発担当 - プロジェクト管理とコーディングの割合 = 50:50 (気持ちは) 最近困っていること Kotlin-festのLTに採択されたはいいものの いまだに運営さんが⼈を間違えていないか⼼配 Twitter connpass -
ϦΞʔΩςΫνϟͷʢͬ͘͟Γʣ λΫγʔं .07ͷһ͚ΞϓϦͷอक͕͠ΜͲ͘ͳ͖ͬͯͨ ˠϦΞʔΩςΫνϟΛ࣮ࢪ ʢ͜͜Βลͷৄ͍͠·ͨ୭͔ผͷਓ͕ͬͯ͘ΕΔͣʣ ͬ͘͟Γͬͨ͜ͱ - γϯάϧϞδϡʔϧ ˠϚϧνϞδϡʔϧ -
ετϦʔϜεύήοςΟ ˠ ίϧʔνϯ 3Y - ΞʔΩςΫνϟ ˠ 'MVY -
ΞʔΩςΫνϟʢ'MVY 3FQPTJUPSZ 4UBUF.BDIJOF 'SBHNFOU "DUJPO $SFBUPS %JTQBUDIFS 4UPSF 3FQPTJUPS
Z "1* %# 1SFG "DUJPO "DUJPO 4UBUF .BDIJOF "DUJPO "DUJWJUZ /BWJHBUJPO (MPCBM"DUJPO '$. .FUFS ֎෦ 3Y -
ϲ݄લͷ͓ؾ࣋ͪද໌ • 6OJU5FTUT • +6OJU • *OUFHSBUJPO5FTUT • 4QFLͰهࡌ •
༷Λग़ྗ͍ͨ͠ͳ͊ • &F5FTUT • มΘΒͳͦ͏ͳͱ͜Ζ͚࣮ͩ • ۀ༻ΞϓϦͳͷͰෳσΟεϓϨΠαΠζΛߟྀ͢Δඞཁ͕ͳ͍ͷͰΓͦ͢͏ɻ ಉ࣌ʹεϓϦϯτຖʹ2"νʔϜʹखಈςετΛ͓ئ͍͍ͯ͘͠ӡ༻ ։ൃऴ͔ྃ࣌ΒϦϦʔε·ͰͷظؒΛͳΔ͘ݮΒ͢త https://codelabs.developers.google.com/codelabs/android-testing/#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 Unit Tests Integration Tests -
ϲ݄ޙͷ͓ؾ࣋ͪ • 6OJU5FTUT • +6OJU • *OUFHSBUJPO5FTUT • 'MVYपΓ͚ͩ4QFLͰهࡌ •
༷Λग़ྗ͍ͨ͠ • &F5FTUT • 6*͕࣍ͰมΘ͍ͬͯ͘ͷͰແཧʢͬͯͨʣ • มΘΒͳͦ͏ͳͱ͜Ζ͚࣮ͩ • ۀ༻ΞϓϦͳͷͰෳσΟεϓϨΠαΠζΛߟྀ͢Δඞཁ͕ͳ͍ͷͰΓͦ͢͏ɻ ಉ࣌ʹεϓϦϯτຖʹ2"νʔϜʹखಈςετΛ͓ئ͍͍ͯ͘͠ӡ༻ ։ൃऴ͔ྃ࣌ΒϦϦʔε·ͰͷظؒΛͳΔ͘ݮΒ͢త https://codelabs.developers.google.com/codelabs/android-testing/#2 -
6OJU5FTUT • ֤Ϟδϡʔϧ͝ͱʹ6OJU5FTUTΛ୶ʑͱ࣮͍ͯ͘͠ • *O.FNPSZͰͷ3PPNςετ • 3FTQPOTFUP&OUJUZͷϚοϐϯάςετ ͳͲ • ϚϧνϞδϡʔϧԽʹΑΓ៉ྷʹґଘ͕ؔΓ͞Ε͍ͯΔͨΊ
ଞϞδϡʔϧ͕ࢀরͰ͖ͳ͍ɻ֤ςετͷཻ͕ἧ͍ͯ͘͢ͱͯ ྑ͍ײ͡ɻ -
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で担保する -
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) -
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) ) -
Scenario(”XXXϘλϯ͕ԡ͞Εͨ߹ɺXXX͕௨͞ΕΔ“) { Given(”εςʔλεΛࢹ͢Δ“) { store.status.observeForever(statusObserver) } When(”XXXϘλϯ͕ԡ͞Εͨ“) { runBlocking
{ actionCreator.consentCarRequest() } } Then(“XXX͕௨͞ΕΔ") { verifySequence { statusObserver.onChanged(DispatchOrderStatus.InProgress) statusObserver.onChanged(DispatchOrderStatus.OnCompleted) } } -
*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ͷΈ ެ։͍ͯ͠Δ -
%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() } -
%C.PEVMF %C$PNQPOFOU %C$PNQPOFOU.PEVMF @Singleton @Component( modules = [ DbModule::class
] ) interface DbComponent { fun requestDatabase(): RequestDatabase -
%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 { -
*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ͱͯ͠ઃఆ͠ ґଘੑΛղܾ͍ͯ͠Δ -
$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() } } -
͜ΕͰ*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͕ੜͰ͖ͨʂ -
@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 -
@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 -
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 -
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 -
ςετ༻ͷϨεϙϯεσʔλ͕ฦͤΔʂ val movApi = DebugApiComponent.builder() .movApi(mockk(relaxed = true)) .build()
.debugApi() .apply { useMockResponse = true } 3FQPTJUPSZ5FTULU ςετ༻ͷϨεϙϯεσʔλ͕ฦͤΔʂ -
࣍ͷνϟϨϯδ • 'MVYͷςετʹؔͯ͠'SBHNFOU͔ΒςετͰ͖ͨΒྑͦ͞͏ • 4QFLΛར༻͢Δ͔Ͳ͏͔Ήʜ • ͋Δఔςετ࣮ํ๏͕ݻ·͖ͬͯͨͷͰɺϓϩδΣΫτϝϯόʔͰڞ༗ • ͪΒ΄ΒΞʔΩςΫνϟʹԊ͍ͬͯͳ͍࣮͕ग़͖͍ͯͯΔͷͰɺςετ͕ॻ͚ͳ͍࣮ ʹͳ͍ͬͯͨΒͯ͠Β͏
• εϓϦϯτ2"࣌ͷෆ۩߹݅ͷԼΛͬͯΈΔ • 2"ݮΛૂͬͯΑΓεϐʔυͷ͋Δ։ൃΛࢦ͢ • ը໘͕ݻ·͖ͬͯͨΒ6*ςετΛࢦ͢ • ·ͩఘΊͯͳ͍ • ಛʹύλʔϯʹΑͬͯ6*͕ΓସΘΔͱ͜Ζ͋ͨΓॻ͖͍ͨ