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

既存コードをAndroid非依存なクラスに抽象化してユニットテストするための第一歩

Avatar for Rui Kowase Rui Kowase
December 08, 2017

 既存コードをAndroid非依存なクラスに抽象化してユニットテストするための第一歩

Avatar for Rui Kowase

Rui Kowase

December 08, 2017
Tweet

More Decks by Rui Kowase

Other Decks in Technology

Transcript

  1. やること • APIクライアントを用意して • onCreate()でAPIクライアントインスタンス化して • ボタンのクリックリスナーにAPIリクエスト処理書いて • レスポンスチェックして •

    成功したらリスト表示して • 失敗したらエラー表示して • etc… もし↑をActivityにそのまま実装していくと・・・ 8
  2. ・・・ 9 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState:

    Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val service = createService() button.setOnClickListener({ service.listRepos(getString(R.string.user)) .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ if (it.isEmpty()) { showError() return@subscribe } showList(it) hideButton() }, { showError() }) }) } private fun createService(): GitHubService { val retrofit = Retrofit.Builder() .baseUrl(getString(R.string.base_url)) .addConverterFactory(MoshiConverterFactory.create()) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .build() return retrofit.create(GitHubService::class.java) } private fun hideButton() { button.visibility = View.GONE } private fun showError() { Toast.makeText(this, getString(R.string.error_message),Toast.LENGTH_LONG).show() private fun showList(it: List<RepoEntity>) { var list = listOf<String>() it.forEach { list += it.name } val adapter = ArrayAdapter<String>(this,android.R.layout.simple_list_item_1, list) listView.adapter = adapter
  3. Repository 15 interface GitHubRepository { fun initService() fun request(user: String):

    Observable<List<RepoEntity>> } class GitHubRepositoryImpl(private val mContext: Context): GitHubRepository { private lateinit var mService: GitHubService override fun initService() { val retrofit = Retrofit.Builder() .baseUrl(mContext.getString(R.string.base_url)) .addConverterFactory(MoshiConverterFactory.create()) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .build() mService = retrofit.create(GitHubService::class.java) } override fun request(user: String): Observable<List<RepoEntity>> = mService.listRepos(user) } Interface Implementation
  4. View/Presenter Interface 17 interface BaseView<T> { var presenter: T }

    class GitHubContract { interface View: BaseView<Presenter> { fun showList(list: List<RepoEntity>) fun showError() fun hideButton() } interface Presenter: BasePresenter { fun request(user: String) } } Base class Interface interface BasePresenter { fun start() }
  5. Presenter Implementation 19 class GitHubPresenter( private val mRepository: GitHubRepository, private

    val mView: GitHubContract.View, private val mSchedulerProvider: BaseSchedulerProvider) : GitHubContract.Presenter { init { mView.presenter = this } override fun start() { mRepository.initService() } override fun request(user: String) { mRepository.request(user) .subscribeOn(mSchedulerProvider.io()) .observeOn(mSchedulerProvider.ui()) .subscribe({ if (it.isEmpty()) { mView.showError() return@subscribe } mView.hideButton() mView.showList(it) }, { mView.showError() }) } }
  6. Presenter Implementation 20 class GitHubPresenter( private val mRepository: GitHubRepository, private

    val mView: GitHubContract.View, private val mSchedulerProvider: BaseSchedulerProvider) : GitHubContract.Presenter { init { mView.presenter = this } override fun start() { mRepository.initService() } override fun request(user: String) { mRepository.request(user) .subscribeOn(mSchedulerProvider.io()) .observeOn(mSchedulerProvider.ui()) .subscribe({ if (it.isEmpty()) { mView.showError() return@subscribe } mView.hideButton() mView.showList(it) }, { mView.showError() }) } }
  7. Presenter Implementation 21 class GitHubPresenter( private val mRepository: GitHubRepository, private

    val mView: GitHubContract.View, private val mSchedulerProvider: BaseSchedulerProvider) : GitHubContract.Presenter { init { mView.presenter = this } override fun start() { mRepository.initService() } override fun request(user: String) { mRepository.request(user) .subscribeOn(mSchedulerProvider.io()) .observeOn(mSchedulerProvider.ui()) .subscribe({ if (it.isEmpty()) { mView.showError() return@subscribe } mView.hideButton() mView.showList(it) }, { mView.showError() }) } }
  8. Presenter Implementation 22 class GitHubPresenter( private val mRepository: GitHubRepository, private

    val mView: GitHubContract.View, private val mSchedulerProvider: BaseSchedulerProvider) : GitHubContract.Presenter { init { mView.presenter = this } override fun start() { mRepository.initService() } override fun request(user: String) { mRepository.request(user) .subscribeOn(mSchedulerProvider.io()) .observeOn(mSchedulerProvider.ui()) .subscribe({ if (it.isEmpty()) { mView.showError() return@subscribe } mView.hideButton() mView.showList(it) }, { mView.showError() }) } }
  9. Activity 24 class MainActivity : AppCompatActivity(), GitHubContract.View { override lateinit

    var presenter: GitHubContract.Presenter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) presenter = GitHubPresenter(GitHubRepositoryImpl(this), this, SchedulerProvider) presenter.start() button.setOnClickListener({ presenter.request(getString(R.string.user)) }) } override fun hideButton() { button.visibility = View.GONE } override fun showError() { Toast.makeText(this, getString(R.string.error_message), Toast.LENGTH_LONG).show() } override fun showList(it: List<RepoEntity>) { var list = listOf<String>() it.forEach { list += it.name } val adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, list) listView.adapter = adapter listView.visibility = View.VISIBLE } }
  10. Activity 25 class MainActivity : AppCompatActivity(), GitHubContract.View { override lateinit

    var presenter: GitHubContract.Presenter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) presenter = GitHubPresenter(GitHubRepositoryImpl(this), this, SchedulerProvider) presenter.start() button.setOnClickListener({ presenter.request(getString(R.string.user)) }) } override fun hideButton() { button.visibility = View.GONE } override fun showError() { Toast.makeText(this, getString(R.string.error_message), Toast.LENGTH_LONG).show() } override fun showList(it: List<RepoEntity>) { var list = listOf<String>() it.forEach { list += it.name } val adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, list) listView.adapter = adapter listView.visibility = View.VISIBLE } }
  11. Activity 26 class MainActivity : AppCompatActivity(), GitHubContract.View { override lateinit

    var presenter: GitHubContract.Presenter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) presenter = GitHubPresenter(GitHubRepositoryImpl(this), this, SchedulerProvider) presenter.start() button.setOnClickListener({ presenter.request(getString(R.string.user)) }) } override fun hideButton() { button.visibility = View.GONE } override fun showError() { Toast.makeText(this, getString(R.string.error_message), Toast.LENGTH_LONG).show() } override fun showList(it: List<RepoEntity>) { var list = listOf<String>() it.forEach { list += it.name } val adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, list) listView.adapter = adapter listView.visibility = View.VISIBLE } }
  12. テスト用にSchedulerを用意する 27 interface BaseSchedulerProvider { fun computation(): Scheduler fun io():

    Scheduler fun ui(): Scheduler } object SchedulerProvider : BaseSchedulerProvider { override fun computation(): Scheduler = Schedulers.computation() override fun io(): Scheduler = Schedulers.io() override fun ui(): Scheduler = AndroidSchedulers.mainThread() } class ImmediateSchedulerProvider : BaseSchedulerProvider { override fun computation(): Scheduler = Schedulers.trampoline() override fun io(): Scheduler = Schedulers.trampoline() override fun ui(): Scheduler = Schedulers.trampoline() } for Implementation for Test
  13. テスト用にSchedulerを用意する 28 interface BaseSchedulerProvider { fun computation(): Scheduler fun io():

    Scheduler fun ui(): Scheduler } object SchedulerProvider : BaseSchedulerProvider { override fun computation(): Scheduler = Schedulers.computation() override fun io(): Scheduler = Schedulers.io() override fun ui(): Scheduler = AndroidSchedulers.mainThread() } class ImmediateSchedulerProvider : BaseSchedulerProvider { override fun computation(): Scheduler = Schedulers.trampoline() override fun io(): Scheduler = Schedulers.trampoline() override fun ui(): Scheduler = Schedulers.trampoline() } for Implementation for Test
  14. テスト用にSchedulerを用意する 29 interface BaseSchedulerProvider { fun computation(): Scheduler fun io():

    Scheduler fun ui(): Scheduler } object SchedulerProvider : BaseSchedulerProvider { override fun computation(): Scheduler = Schedulers.computation() override fun io(): Scheduler = Schedulers.io() override fun ui(): Scheduler = AndroidSchedulers.mainThread() } class ImmediateSchedulerProvider : BaseSchedulerProvider { override fun computation(): Scheduler = Schedulers.trampoline() override fun io(): Scheduler = Schedulers.trampoline() override fun ui(): Scheduler = Schedulers.trampoline() } for Implementation for Test
  15. Presenter Implementation 30 class GitHubPresenter( private val mRepository: GitHubRepository, private

    val mView: GitHubContract.View, private val mSchedulerProvider: BaseSchedulerProvider) : GitHubContract.Presenter { init { mView.presenter = this } override fun start() { mRepository.initService() } override fun request(user: String) { mRepository.request(user) .subscribeOn(mSchedulerProvider.io()) .observeOn(mSchedulerProvider.ui()) .subscribe({ if (it.isEmpty()) { mView.showError() return@subscribe } mView.hideButton() mView.showList(it) }, { mView.showError() }) } }
  16. テストを書く(セットアップ) 32 class GitHubPresenterTest { @Mock private lateinit var mRepository:

    GitHubRepository @Mock private lateinit var mView: GitHubContract.View private lateinit var mPresenter: GitHubPresenter private lateinit var mSchedulerProvider: BaseSchedulerProvider @Before fun setUp() { MockitoAnnotations.initMocks(this) mSchedulerProvider = ImmediateSchedulerProvider() mPresenter = GitHubPresenter(mRepository, mView, mSchedulerProvider) }
  17. テストを書く(セットアップ) 33 class GitHubPresenterTest { @Mock private lateinit var mRepository:

    GitHubRepository @Mock private lateinit var mView: GitHubContract.View private lateinit var mPresenter: GitHubPresenter private lateinit var mSchedulerProvider: BaseSchedulerProvider @Before fun setUp() { MockitoAnnotations.initMocks(this) mSchedulerProvider = ImmediateSchedulerProvider() mPresenter = GitHubPresenter(mRepository, mView, mSchedulerProvider) }
  18. テストを書く(セットアップ) 34 class GitHubPresenterTest { @Mock private lateinit var mRepository:

    GitHubRepository @Mock private lateinit var mView: GitHubContract.View private lateinit var mPresenter: GitHubPresenter private lateinit var mSchedulerProvider: BaseSchedulerProvider @Before fun setUp() { MockitoAnnotations.initMocks(this) mSchedulerProvider = ImmediateSchedulerProvider() mPresenter = GitHubPresenter(mRepository, mView, mSchedulerProvider) }
  19. テストを書く(テストケース) 35 @Test fun start() { mPresenter.start() verify(mRepository).initService() } private

    fun request() = mRepository.request(USER) @Test fun requestSuccess() { val list = listOf(RepoEntity("name")) `when`(request()).thenReturn(Observable.just(list)) mPresenter.request(USER) verify(mView).showList(list) verify(mView).hideButton() } @Test fun requestError() { `when`(request()).thenReturn(Observable.error(Exception())) mPresenter.request(USER) verify(mView).showError() } @Test fun requestEmpty() { `when`(request()).thenReturn(Observable.just(listOf())) mPresenter.request(USER) verify(mView).showError() }
  20. テストを書く(テストケース) 36 @Test fun start() { mPresenter.start() verify(mRepository).initService() } private

    fun request() = mRepository.request(USER) @Test fun requestSuccess() { val list = listOf(RepoEntity("name")) `when`(request()).thenReturn(Observable.just(list)) mPresenter.request(USER) verify(mView).showList(list) verify(mView).hideButton() } @Test fun requestError() { `when`(request()).thenReturn(Observable.error(Exception())) mPresenter.request(USER) verify(mView).showError() } @Test fun requestEmpty() { `when`(request()).thenReturn(Observable.just(listOf())) mPresenter.request(USER) verify(mView).showError() }
  21. テストを書く(テストケース) 37 @Test fun start() { mPresenter.start() verify(mRepository).initService() } private

    fun request() = mRepository.request(USER) @Test fun requestSuccess() { val list = listOf(RepoEntity("name")) `when`(request()).thenReturn(Observable.just(list)) mPresenter.request(USER) verify(mView).showList(list) verify(mView).hideButton() } @Test fun requestError() { `when`(request()).thenReturn(Observable.error(Exception())) mPresenter.request(USER) verify(mView).showError() } @Test fun requestEmpty() { `when`(request()).thenReturn(Observable.just(listOf())) mPresenter.request(USER) verify(mView).showError() }
  22. テストを書く(テストケース) 38 @Test fun start() { mPresenter.start() verify(mRepository).initService() } private

    fun request() = mRepository.request(USER) @Test fun requestSuccess() { val list = listOf(RepoEntity("name")) `when`(request()).thenReturn(Observable.just(list)) mPresenter.request(USER) verify(mView).showList(list) verify(mView).hideButton() } @Test fun requestError() { `when`(request()).thenReturn(Observable.error(Exception())) mPresenter.request(USER) verify(mView).showError() } @Test fun requestEmpty() { `when`(request()).thenReturn(Observable.just(listOf())) mPresenter.request(USER) verify(mView).showError() }
  23. テストを書く(テストケース) 39 @Test fun start() { mPresenter.start() verify(mRepository).initService() } private

    fun request() = mRepository.request(USER) @Test fun requestSuccess() { val list = listOf(RepoEntity("name")) `when`(request()).thenReturn(Observable.just(list)) mPresenter.request(USER) verify(mView).showList(list) verify(mView).hideButton() } @Test fun requestError() { `when`(request()).thenReturn(Observable.error(Exception())) mPresenter.request(USER) verify(mView).showError() } @Test fun requestEmpty() { `when`(request()).thenReturn(Observable.just(listOf())) mPresenter.request(USER) verify(mView).showError() }
  24. 参考 googlesamples/android-architecture: A collection of samples to discuss and showcase

    different architectural tools and patterns for Android apps. https://github.com/googlesamples/android-architecture bufferapp/android-clean-architecture-boilerplate: An android boilerplate project using clean architecture https://github.com/bufferapp/android-clean-architecture-boilerplate 44