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

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

Rui Kowase
December 08, 2017

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

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