Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

今日の話 4

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

サンプルアプリ 7 仕様 ● 初期状態はボタンが表示されている ● ボタンを押すとリポジトリ一覧を 表示し、ボタンを消す ● 異常時(結果0件、取得失敗)は エラーメッセージを表示する コード https://github.com/rkowase/android-mvp-sample

Slide 8

Slide 8 text

やること ● APIクライアントを用意して ● onCreate()でAPIクライアントインスタンス化して ● ボタンのクリックリスナーにAPIリクエスト処理書いて ● レスポンスチェックして ● 成功したらリスト表示して ● 失敗したらエラー表示して ● etc… もし↑をActivityにそのまま実装していくと・・・ 8

Slide 9

Slide 9 text

・・・ 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) { var list = listOf() it.forEach { list += it.name } val adapter = ArrayAdapter(this,android.R.layout.simple_list_item_1, list) listView.adapter = adapter

Slide 10

Slide 10 text

10 Fat(になりそうな)Activityのできあがり!

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

Repository 15 interface GitHubRepository { fun initService() fun request(user: String): Observable> } 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> = mService.listRepos(user) } Interface Implementation

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

View/Presenter Interface 17 interface BaseView { var presenter: T } class GitHubContract { interface View: BaseView { fun showList(list: List) fun showError() fun hideButton() } interface Presenter: BasePresenter { fun request(user: String) } } Base class Interface interface BasePresenter { fun start() }

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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() }) } }

Slide 20

Slide 20 text

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() }) } }

Slide 21

Slide 21 text

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() }) } }

Slide 22

Slide 22 text

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() }) } }

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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) { var list = listOf() it.forEach { list += it.name } val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, list) listView.adapter = adapter listView.visibility = View.VISIBLE } }

Slide 25

Slide 25 text

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) { var list = listOf() it.forEach { list += it.name } val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, list) listView.adapter = adapter listView.visibility = View.VISIBLE } }

Slide 26

Slide 26 text

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) { var list = listOf() it.forEach { list += it.name } val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, list) listView.adapter = adapter listView.visibility = View.VISIBLE } }

Slide 27

Slide 27 text

テスト用に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

Slide 28

Slide 28 text

テスト用に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

Slide 29

Slide 29 text

テスト用に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

Slide 30

Slide 30 text

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() }) } }

Slide 31

Slide 31 text

完成! 31

Slide 32

Slide 32 text

テストを書く(セットアップ) 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) }

Slide 33

Slide 33 text

テストを書く(セットアップ) 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) }

Slide 34

Slide 34 text

テストを書く(セットアップ) 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) }

Slide 35

Slide 35 text

テストを書く(テストケース) 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() }

Slide 36

Slide 36 text

テストを書く(テストケース) 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() }

Slide 37

Slide 37 text

テストを書く(テストケース) 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() }

Slide 38

Slide 38 text

テストを書く(テストケース) 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() }

Slide 39

Slide 39 text

テストを書く(テストケース) 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() }

Slide 40

Slide 40 text

All 4 tests passed ! 40

Slide 41

Slide 41 text

次のステップ ● Domain層追加 ● RepositoryをRemoteとLocalに分割 ● DIライブラリ導入 ● AAC導入 ● モジュール分割 ● テストフレームワーク検討 41

Slide 42

Slide 42 text

色々頑張るとこうなる 42 (引用) https://github.com/bufferapp/android-clean-architecture-boilerplate

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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