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

3be52a14c8c58209263d7f83204b8dc7?s=47 Rui Kowase
December 08, 2017

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

3be52a14c8c58209263d7f83204b8dc7?s=128

Rui Kowase

December 08, 2017
Tweet

Transcript

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

  2. 自己紹介 • 名前: 小和瀬 塁(こわせ るい) • アカウント: @rkowase 2

  3. Qiita 3 https://qiita.com/rkowase/items/54dc44537e9af519023e

  4. 今日の話 4

  5. ゴール 既存コードをAndroid非依存なクラスに 抽象化してユニットテストするための 第一歩を踏み出してもらう 5 (踏み出すための工数をどうやって確保するかなどの社内政治的な話はしません)

  6. Android非依存なクラスとは Android Frameworkに依存していないクラスのこ とです。 具体的にはContextなどに依存してない ピュアJava/Kotlinのクラスを指します。 6

  7. サンプルアプリ 7 仕様 • 初期状態はボタンが表示されている • ボタンを押すとリポジトリ一覧を 表示し、ボタンを消す • 異常時(結果0件、取得失敗)は

    エラーメッセージを表示する コード https://github.com/rkowase/android-mvp-sample
  8. やること • APIクライアントを用意して • onCreate()でAPIクライアントインスタンス化して • ボタンのクリックリスナーにAPIリクエスト処理書いて • レスポンスチェックして •

    成功したらリスト表示して • 失敗したらエラー表示して • etc… もし↑をActivityにそのまま実装していくと・・・ 8
  9. ・・・ 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
  10. 10 Fat(になりそうな)Activityのできあがり!

  11. 解決したい主な課題 • 機能拡張につれてどんどんFatになる • テストがしにくい ↑なんとかしたい! 11

  12. MVPアーキテクチャ 12 (引用) https://github.com/googlesamples/android-architecture/tree/todo-mvp/

  13. サンプルアプリのアーキテクチャ 13 Activity UI Presentation Data Presenter Interface Presenter Implementation

    Repository Interface Repository Implementation View Interface API Service
  14. サンプルアプリのアーキテクチャ 14 Activity UI Presentation Data Presenter Interface Presenter Implementation

    Repository Interface Repository Implementation View Interface API Service
  15. 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
  16. サンプルアプリのアーキテクチャ 16 Activity UI Presentation Data Presenter Interface Presenter Implementation

    Repository Interface Repository Implementation View Interface API Service
  17. 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() }
  18. サンプルアプリのアーキテクチャ 18 Activity UI Presentation Data Presenter Interface Presenter Implementation

    Repository Interface Repository Implementation View Interface API Service
  19. 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() }) } }
  20. 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() }) } }
  21. 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() }) } }
  22. 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() }) } }
  23. サンプルアプリのアーキテクチャ 23 Activity UI Presentation Data Presenter Interface Presenter Implementation

    Repository Interface Repository Implementation View Interface API Service
  24. 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 } }
  25. 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 } }
  26. 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 } }
  27. テスト用に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
  28. テスト用に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
  29. テスト用に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
  30. 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() }) } }
  31. 完成! 31

  32. テストを書く(セットアップ) 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) }
  33. テストを書く(セットアップ) 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) }
  34. テストを書く(セットアップ) 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) }
  35. テストを書く(テストケース) 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() }
  36. テストを書く(テストケース) 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() }
  37. テストを書く(テストケース) 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() }
  38. テストを書く(テストケース) 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() }
  39. テストを書く(テストケース) 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() }
  40. All 4 tests passed ! 40

  41. 次のステップ • Domain層追加 • RepositoryをRemoteとLocalに分割 • DIライブラリ導入 • AAC導入 •

    モジュール分割 • テストフレームワーク検討 41
  42. 色々頑張るとこうなる 42 (引用) https://github.com/bufferapp/android-clean-architecture-boilerplate

  43. まとめ • 各レイヤーをInterfaceで区切る • ロジック部分をピュアJava/Kotlinにする • Mockitoを使ってオブジェクトをモックする • テスト時はSchedulerを差し替える 43

  44. 参考 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