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

How to improve your MVP architecture and tests

How to improve your MVP architecture and tests

for DroidKaigi2018

きりみん

February 07, 2018
Tweet

More Decks by きりみん

Other Decks in Programming

Transcript

  1. paymo's Architecture %BUB૚ %PNBJO૚ 1SFTFOUBUJPO૚ 7JFX 1SFTFOUFS 6TF$BTF 3FQPTJUPSZ %BUB4UPSF

    "1* %# 1SFGFSFODFT "DUJWJUZ 'SBHNFOU $VTUPN7JFX 3FQPTJUPSZ %BUB4UPSF 3FQPTJUPSZ %BUB4UPSF .PEFM %* %* %*
  2. MVP

  3. 1SFTFOUFS 7JFX 6TF$BTF ඳը "OESPJE '8 %# "1* 4IBSFE 1SFGFSFODFT

    ViewͱPresenterΛ෼཭͢Δ • Androidʹґଘͨ͠ॲཧ΍σʔλΞΫηεॲཧΛ Presenter͔Β෼཭͠ɺPresenterΛϐϡΞʹอͭ
  4. class TopActivity : AppCompatActivity() { lateinit var presenter: TopPresenter override

    fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_top) presenter = TopPresenter(this) presenter.onCreate() toolbar.title = "Who on GitHub" submitButton.setOnClickListener { hideKeyBoard() parentLayout.visibility = View.GONE presenter.getUserInfo(userIdEditText) } userIdEditText.setOnKeyListener { view, keyCode, event -> if (KeyEvent.KEYCODE_ENTER == keyCode && event.action == KeyEvent.ACTION_UP) { hideKeyBoard() parentLayout.visibility = View.GONE presenter.getUserInfo(userIdEditText) } false } } ... fun setUserInfo(user: User) { parentLayout.visibility = View.VISIBLE nameText.text = user.name if (user.location.isNullOrEmpty()) { locationText.visibility = View.GONE } else { locationText.visibility = View.VISIBLE locationText.text = user.location } if (user.mail.isNullOrEmpty()) { mailText.visibility = View.GONE } else { mailText.visibility = View.VISIBLE mailText.text = user.mail } if (user.link.isNullOrEmpty()) { linkText.visibility = View.GONE } else { linkText.visibility = View.VISIBLE linkText.text = user.link } Picasso.with(this).load(user.iconUrl).fit().into(iconImage) } ...
  5. override fun onCreate(savedInstanceState: Bundle?) { ... submitButton.setOnClickListener { hideKeyBoard() parentLayout.visibility

    = View.GONE presenter.getUserInfo(userIdEditText) } ... fun setUserInfo(user: User) { parentLayout.visibility = View.VISIBLE nameText.text = user.name if (user.location.isNullOrEmpty()) { locationText.visibility = View.GONE } else { locationText.visibility = View.VISIBLE locationText.text = user.location } if (user.mail.isNullOrEmpty()) { mailText.visibility = View.GONE } else { mailText.visibility = View.VISIBLE mailText.text = user.mail } if (user.link.isNullOrEmpty()) { linkText.visibility = View.GONE } else { linkText.visibility = View.VISIBLE linkText.text = user.link } Picasso.with(this).load(user.iconUrl).fit().into(iconImage) } 7JFXଆͰϩδοΫΛ ࣋ͬͯΔ 7JFX͕1SFTFOUFSʹΠϕϯτΛ௨஌ ͢ΔͷͰ͸ͳ͘ɺࢦࣔΛग़͍ͯ͠Δ
  6. class TopPresenter(val view: TopActivity) { val useCase = TopUseCase() lateinit

    var disposables: CompositeDisposable fun onCreate() { disposables = CompositeDisposable() } fun onDestroy() { disposables.clear() } fun getUserInfo(userIdEditText: EditText) { val id = userIdEditText.text.toString() if (id.isEmpty()) { Toast.makeText(view, "validation error", Toast.LENGTH_SHORT).show() return } val progressBar = view.findViewById<ProgressBar>(R.id.progressBar) progressBar.visibility = View.VISIBLE useCase.fetchUserInfo(id).subscribe({ user -> progressBar.visibility = View.GONE view.setUserInfo(user) view.setLanguages(getLanguages(user.repositories)) view.setRepositories(sortRepositories(user.repositories)) }, { view.progressBar.visibility = View.GONE Toast.makeText(view, "network error", Toast.LENGTH_SHORT).show() }).also { disposables.add(it) } } /** * ϦϙδτϦΛݴޠ͝ͱʹݴޠ໊ͱϦϙδτϦϦετͷPairʹ·ͱΊͨListΛฦ͢ */ fun getLanguages(repositories: List<Repository>): List<Pair<String, List<Repository>>> { return repositories .filter { it.language != null } .groupBy { it.language!! } .toList().sortedByDescending { language -> language.second.count() } } /** * ϦϙδτϦΛελʔͷ਺ͱfork͔൱͔Ͱιʔτͯ͠ฦ͢ */ fun sortRepositories(repositories: List<Repository>): List<Repository> { return repositories .sortedByDescending { repo -> repo.starCount } .sortedBy { repo -> repo.isFork } }
  7. class TopPresenter(val view: TopActivity) { ... fun getUserInfo(userIdEditText: EditText) {

    val id = userIdEditText.text.toString() if (id.isEmpty()) { Toast.makeText(view, "validation error", Toast.LENGTH_SHORT).show() return } val progressBar = view.findViewById<ProgressBar>(R.id.progressBar) progressBar.visibility = View.VISIBLE useCase.fetchUserInfo(id).subscribe({ user -> progressBar.visibility = View.GONE view.setUserInfo(user) view.setLanguages(getLanguages(user.repositories)) view.setRepositories(sortRepositories(user.repositories)) }, { view.progressBar.visibility = View.GONE Toast.makeText(view, "network error", Toast.LENGTH_SHORT).show() }).also { disposables.add(it) } } WJFXΛ"DUJWJUZͱͯ͠ ࢀর͍ͯ͠Δ pOE7JFX#Z*EͰ 1SPHSFTT#BSΛ ௚઀ૢ࡞ͯ͠Δ 5PTUͷදࣔͳͲ΋ 1SFTFOUFSଆͰߦ͍ͬͯΔ
  8. interface TopView { class TopActivity : AppCompatActivity(), TopView { lateinit

    var presenter: TopPresenter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) presenter = TopPresenter(this, TopUseCase()) presenter.onCreate() } override fun onDestroy() { presenter.onDestroy() super.onDestroy() } override fun initView() { setContentView(R.layout.activity_top) toolbar.title = "Who on GitHub" submitButton.setOnClickListener { presenter.onSubmitButtonClick(userIdEditText.text.toString()) } userIdEditText.setOnKeyListener { _, keyCode, event -> presenter.onUserIdEditTextKeyListener(userIdEditText.text.toString(), keyCode, eve false } } ... "DUJWJUZΛ 7JFXΠϯλʔϑΣΠεͰӅṭ 7JFX͔Β͸QSFTFOUFSʹ ϥΠϑαΠΫϧͳͲͷ ΠϕϯτΛ௨஌͢Δ͚ͩ $MJDLΠϕϯτͳͲ΋ 1SFTFOUFSʹ௨஌͢ΔͷΈ
  9. ... override fun showErrorToast(text: String) { Toast.makeText(this, text, Toast.LENGTH_SHORT).show() }

    override fun setProgressBarVisibility(visibility: Int) { progressBar.visibility = visibility } override fun setParentLayoutVisibility(visibility: Int) { parentLayout.visibility = visibility } override fun hideKeyBoard() { val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager imm.hideSoftInputFromWindow(currentFocus!!.windowToken, InputMethodManager.HIDE_NOT_ALWAYS) } override fun setUserName(name: String) { nameText.text = name } override fun setLocationTextAndVisibility(visibility: Int, text: String) { locationText.visibility = visibility locationText.text = text } override fun setLinkTextAndVisibility(visibility: Int, text: String) { linkText.visibility = visibility linkText.text = text } override fun setMailTextAndVisibility(visibility: Int, text: String) { mailText.visibility = visibility mailText.text = text } override fun setIcon(iconUrl: String) { Picasso.with(this).load(iconUrl).fit().into(iconImage) } override fun setIconVisibility(visibility: Int) { iconImage.visibility = visibility } ... ϩδοΫΛ࣋ͨͳ͍ 7JFXΛ௚઀ૢ࡞͢Δ ୹͍ϝιου܈Λ࣋ͭ
  10. } fun initView() fun showErrorToast(text: String) fun setProgressBarVisibility(visibility: Int) fun

    setParentLayoutVisibility(visibility: Int) fun hideKeyBoard() fun setUserName(name: String) fun setLocationTextAndVisibility(visibility: Int, text: String) fun setMailTextAndVisibility(visibility: Int, text: String) fun setLinkTextAndVisibility(visibility: Int, text: String) fun setIcon(iconUrl: String) fun setIconVisibility(visibility: Int) fun addLanguageView(language: Language) fun addRepositoryView(repository: Repository) } ͦΕΒͷϝιου͸ *OUFSGBDFʹఆٛ͞Ε 1SFTFOUFS͔ΒΞΫηεՄೳ
  11. class TopPresenter(val view: TopView, val useCase: TopUseCase) { lateinit var

    disposables: CompositeDisposable fun onCreate() { disposables = CompositeDisposable() view.initView() } fun onDestroy() { disposables.clear() } fun onSubmitButtonClick(text: String) { getUserInfo(text) } fun onUserIdEditTextKeyListener(text: String, keyCode: Int, action: Int) { if (KeyEvent.KEYCODE_ENTER == keyCode && action == KeyEvent.ACTION_UP) { getUserInfo(text) } } ... "DUJWJUZΛ7JFXΠϯλʔϑΣΠε ͱͯ͠อ࣋ Πϕϯτ௨஌Λड͚औͬͨ 1SFTFOUFS͸ΠϕϯτʹԠͨ͡ॲཧΛߦ͏
  12. private fun getUserInfo(id: String) { if (id.isEmpty()) { view.showErrorToast("validation error")

    return } view.hideKeyBoard() view.setProgressBarVisibility(View.VISIBLE) view.setParentLayoutVisibility(View.GONE) useCase.fetchUserInfo(id).subscribe({ user -> view.setProgressBarVisibility(View.GONE) view.setParentLayoutVisibility(View.VISIBLE) view.setUserName(user.name) if (user.location.isNullOrEmpty()) { view.setLocationTextAndVisibility(View.GONE, "") } else { view.setLocationTextAndVisibility(View.VISIBLE, user.location!!) } if (user.mail.isNullOrEmpty()) { view.setMailTextAndVisibility(View.GONE, "") } else { view.setMailTextAndVisibility(View.VISIBLE, user.mail!!) } if (user.link.isNullOrEmpty()) { view.setLinkTextAndVisibility(View.GONE, "") } else { view.setLinkTextAndVisibility(View.VISIBLE, user.link!!) } if (user.iconUrl != null) { view.setIconVisibility(View.VISIBLE) view.setIcon(user.iconUrl) } else { view.setIconVisibility(View.INVISIBLE) } getLanguages(user.repositories).forEach { view.addLanguageView(it) } sortRepositories(user.repositories).forEach { view.addRepositoryView(it) } }, { view.setProgressBarVisibility(View.GONE) view.showErrorToast("network error") }).also { disposables.add(it) } } औಘͨ͠σʔλΛ1SFTFOUFSଆͰ൑ఆ͠ɺ ࠷ऴతͳ݁ՌΛ7JFXʹରͯ͠ࢦࣔ͢Δ 1SFTFOUFS͔Β6TF$BTFʹ σʔλͷऔಘΛґཔ
  13. 1SFTFOUFS 7JFX 6TF$BTF ඳը "OESPJE '8 %# "1* 4IBSFE 1SFGFSFODFT

    ͓͞Β͍ • Androidʹґଘͨ͠ॲཧ΍σʔλΞΫηεॲཧΛ Presenter͔Β෼཭͠ɺPresenterΛϐϡΞʹอͭ
  14. MockitoΛ࢖͏ • JavaͷϝδϟʔͳϞοΫϥΠϒϥϦ • Ϋϥε΍ΠϯλʔϑΣΠεͷϞοΫΠϯελϯεΛੜ ੒͠ɺϝιουʹ೚ҙͷ໭Γ஋Λࢦఆͨ͠Γɺϝιο υ͕ݺ͹Εͨճ਺Λ֬ೝͨ͠Γग़དྷΔ // TopViewͷϞοΫΠϯελϯεΛੜ੒ viewMock

    = mock(TopView::class.java) // presenterͷonCreate()ϝιουΛςετ಺ͰݺͿ presenter = TopPresenter(viewMock, useCaseMock) presenter.onCreate() // ݁ՌviewMockͷinitView()ϝιου͕ݺ͹Ε͍ͯΔࣄΛ֬ೝ verify(viewMock, Mockito.times(1)).initView()
  15. class TopPresenterTest { lateinit var viewMock: TopView lateinit var useCaseMock:

    TopUseCase lateinit var presenter: TopPresenter @Before fun setup() { viewMock = mock(TopView::class.java) useCaseMock = mock(TopUseCase::class.java) presenter = TopPresenter(viewMock, useCaseMock) } @Test fun onCreateTest() { presenter.onCreate() // initView()ϝιου͕ݺ͹Ε͍ͯΔࣄΛ֬ೝ verify(viewMock, Mockito.times(1)).initView() } @Test fun onSubmitButtonClickTestWithEmptyText() { presenter.onCreate() presenter.onSubmitButtonClick("") // ೖྗ͞Εͨจࣈྻ͕ۭͷ৔߹͸Τϥʔ͕දࣔ͞Εॲཧ͕ऴྃ͢Δ͜ͱΛ֬ೝ verify(viewMock, times(1)).showErrorToast("validation error") verify(viewMock, never()).setProgressBarVisibility(View.VISIBLE) verify(useCaseMock, never()).fetchUserInfo(anyString()) } @Test fun onSubmitButtonClickTestWithText() { Mockito.`when`(useCaseMock.fetchUserInfo("kirimin")).thenReturn(Single.never()) presenter.onCreate() presenter.onSubmitButtonClick("kirimin") // ೖྗ͞Εͨจࣈྻ͕ۭͷ৔߹͸Τϥʔ͕දࣔ͞Εͳ͍ࣄΛ֬ೝ verify(viewMock, never()).showErrorToast("validation error") // ೖྗ͞ΕͨจࣈྻΛ࢖༻ͯ͠σʔλͷऔಘ͕ߦΘΕΔ͜ͱΛ֬ೝ verify(viewMock, times(1)).setParentLayoutVisibility(View.GONE) verify(viewMock, times(1)).setProgressBarVisibility(View.VISIBLE) verify(viewMock, never()).setParentLayoutVisibility(View.VISIBLE) verify(viewMock, never()).setProgressBarVisibility(View.GONE) verify(useCaseMock, times(1)).fetchUserInfo("kirimin") } @Test fun onFetchUserInfoSuccessMinCaseTest() { // UseCase͕ฦ͢஋ΛϞοΫ val userEntity = UserEntity(name = "kirimin", location = null, company = null, blog = null, email = null, avatar_url = null) val repoEntity = RepositoryEntity() Mockito.`when`(useCaseMock.fetchUserInfo("kirimin")).thenReturn(Single.just(User(userEntity, listOf(repoEntity)))) presenter.onCreate() presenter.onSubmitButtonClick("kirimin") verify(viewMock, times(1)).setProgressBarVisibility(View.GONE) verify(viewMock, times(1)).setParentLayoutVisibility(View.VISIBLE) verify(viewMock, times(1)).setUserName("kirimin") verify(viewMock, times(1)).setLocationTextAndVisibility(eq(View.GONE), anyString()) verify(viewMock, times(1)).setMailTextAndVisibility(eq(View.GONE), anyString()) verify(viewMock, times(1)).setLinkTextAndVisibility(eq(View.GONE), anyString()) verify(viewMock, times(1)).setIconVisibility(View.INVISIBLE) }
  16. @Test fun onSubmitButtonClickTestWithEmptyText() { presenter.onCreate() presenter.onSubmitButtonClick("") // ೖྗ͞Εͨจࣈྻ͕ۭͷ৔߹͸Τϥʔ͕දࣔ͞Εॲཧ͕ऴྃ͢Δ͜ͱΛ֬ೝ verify(viewMock, times(1)).showErrorToast("validation

    error") verify(viewMock, never()).setProgressBarVisibility(View.VISIBLE) verify(useCaseMock, never()).fetchUserInfo(anyString()) } @Test fun onSubmitButtonClickTestWithText() { Mockito.`when`(useCaseMock.fetchUserInfo("kirimin")).thenReturn(Single.n presenter.onCreate() presenter.onSubmitButtonClick("kirimin") // ೖྗ͞Εͨจࣈྻ͕ۭͷ৔߹͸Τϥʔ͕දࣔ͞Εͳ͍ࣄΛ֬ೝ verify(viewMock, never()).showErrorToast("validation error") // ೖྗ͞ΕͨจࣈྻΛ࢖༻ͯ͠σʔλͷऔಘ͕ߦΘΕΔ͜ͱΛ֬ೝ verify(viewMock, times(1)).setParentLayoutVisibility(View.GONE) verify(viewMock, times(1)).setProgressBarVisibility(View.VISIBLE) verify(viewMock, never()).setParentLayoutVisibility(View.VISIBLE) verify(viewMock, never()).setProgressBarVisibility(View.GONE) verify(useCaseMock, times(1)).fetchUserInfo("kirimin") }
  17. @Test fun onFetchUserInfoSuccessMinCaseTest() { val userEntity = UserEntity(name = "kirimin",

    location = null, company = null, blog = null, email = null, a val repoEntity = RepositoryEntity() Mockito.`when`(useCaseMock.fetchUserInfo("kirimin")).thenReturn(Single.just(User(userEntity, listOf(repoEn presenter.onCreate() presenter.onSubmitButtonClick("kirimin") verify(viewMock, times(1)).setProgressBarVisibility(View.GONE) verify(viewMock, times(1)).setParentLayoutVisibility(View.VISIBLE) verify(viewMock, times(1)).setUserName("kirimin") verify(viewMock, times(1)).setLocationTextAndVisibility(eq(View.GONE), anyString()) verify(viewMock, times(1)).setMailTextAndVisibility(eq(View.GONE), anyString()) verify(viewMock, times(1)).setLinkTextAndVisibility(eq(View.GONE), anyString()) verify(viewMock, times(1)).setIconVisibility(View.INVISIBLE) } @Test fun onFetchUserInfoMaxCaseTest() { val userEntity = UserEntity(name = "kirimin", location = "tokyo, japan", company = "kirimin inc.", blog = " email = "[email protected]", avatar_url = "http://kirimin.me/face_icon.png") val repoEntity = RepositoryEntity() Mockito.`when`(useCaseMock.fetchUserInfo("kirimin")).thenReturn(Single.just(User(userEntity, listOf(repoEn presenter.onCreate() presenter.onSubmitButtonClick("kirimin") verify(viewMock, times(1)).setProgressBarVisibility(View.GONE) verify(viewMock, times(1)).setParentLayoutVisibility(View.VISIBLE) verify(viewMock, times(1)).setUserName("kirimin") verify(viewMock, times(1)).setLocationTextAndVisibility(View.VISIBLE, "tokyo, japan") verify(viewMock, times(1)).setMailTextAndVisibility(View.VISIBLE, "[email protected]") verify(viewMock, times(1)).setLinkTextAndVisibility(View.VISIBLE, "http://kirimin.me") verify(viewMock, times(1)).setIconVisibility(View.VISIBLE) verify(viewMock, times(1)).setIcon("http://kirimin.me/face_icon.png") }
  18. @Test fun onFetchUserInfoSuccessMinCaseTest() { // UseCase͕ฦ͢஋ΛϞοΫ val userEntity = UserEntity(name

    = "kirimin", location = null, company = null, blog = null, email = null, avatar_url = null) val repoEntity = RepositoryEntity() Mockito.`when`(useCaseMock.fetchUserInfo("kirimin")).thenReturn(Single.just(User(userEntity, listOf(repoEnti presenter.onCreate() presenter.onSubmitButtonClick("kirimin") verify(viewMock, times(1)).setProgressBarVisibility(View.GONE) verify(viewMock, times(1)).setParentLayoutVisibility(View.VISIBLE) verify(viewMock, times(1)).setUserName("kirimin") verify(viewMock, times(1)).setLocationTextAndVisibility(eq(View.GONE), anyString()) verify(viewMock, times(1)).setMailTextAndVisibility(eq(View.GONE), anyString()) verify(viewMock, times(1)).setLinkTextAndVisibility(eq(View.GONE), anyString()) verify(viewMock, times(1)).setIconVisibility(View.INVISIBLE) } @Test fun onFetchUserInfoMaxCaseTest() { // UseCase͕ฦ͢஋ΛϞοΫ val userEntity = UserEntity(name = "kirimin", location = "tokyo, japan", company = "kirimin inc.", blog = "h kirimin.me", email = "[email protected]", avatar_url = "http://kirimin.me/face_icon.png") val repoEntity = RepositoryEntity() Mockito.`when`(useCaseMock.fetchUserInfo("kirimin")).thenReturn(Single.just(User(userEntity, listOf(repoEnti presenter.onCreate() presenter.onSubmitButtonClick("kirimin") verify(viewMock, times(1)).setProgressBarVisibility(View.GONE) verify(viewMock, times(1)).setParentLayoutVisibility(View.VISIBLE) verify(viewMock, times(1)).setUserName("kirimin") verify(viewMock, times(1)).setLocationTextAndVisibility(View.VISIBLE, "tokyo, japan") verify(viewMock, times(1)).setMailTextAndVisibility(View.VISIBLE, "[email protected]") verify(viewMock, times(1)).setLinkTextAndVisibility(View.VISIBLE, "http://kirimin.me") verify(viewMock, times(1)).setIconVisibility(View.VISIBLE) verify(viewMock, times(1)).setIcon("http://kirimin.me/face_icon.png") } σʔλͷύλʔϯ͝ͱʹ ςετϝιουΛॻ͍͍ͯΔ ͦΕͧΕͷςετͰ ॏෳίʔυΛॻ͍ͯΔ σʔλύλʔϯ΍ ঢ়ଶ͕૿͑Δ΄Ͳ ςετίʔυ͕ංେԽ͢Δ
  19. @Test fun userIdSubmitTest() { whenever(useCaseMock.fetchUserInfo(anyString())).thenReturn(Single.never()) fun isError() { verify(viewMock, times(1)).showErrorToast(anyString())

    verify(viewMock, never()).setProgressBarVisibility(View.VISIBLE) verify(useCaseMock, never()).fetchUserInfo(anyString()) } fun isSuccess() { verify(viewMock, never()).showErrorToast(anyString()) verify(viewMock, times(1)).setParentLayoutVisibility(View.GONE) verify(viewMock, times(1)).setProgressBarVisibility(View.VISIBLE) verify(viewMock, never()).setParentLayoutVisibility(View.VISIBLE) verify(viewMock, never()).setProgressBarVisibility(View.GONE) verify(useCaseMock, times(1)).fetchUserInfo("kirimin") } fun isIgnore() { verify(viewMock, never()).showErrorToast(anyString()) verify(viewMock, never()).setProgressBarVisibility(anyInt()) verify(viewMock, never()).setParentLayoutVisibility(anyInt()) verify(useCaseMock, never()).fetchUserInfo(anyString()) } fun isSubmitButtonClick() { initializeMocks() presenter.onSubmitButtonClick("") isError() initializeMocks() presenter.onSubmitButtonClick("kirimin") isSuccess() } fun withEditTextKeyEnter() { initializeMocks() presenter.onUserIdEditTextKeyListener("", KeyEvent.KEYCODE_ENTER, KeyEvent.ACTION_UP) isError() initializeMocks() presenter.onUserIdEditTextKeyListener("kirimin", KeyEvent.KEYCODE_ENTER, KeyEvent.ACTION_UP) isSuccess() initializeMocks() presenter.onUserIdEditTextKeyListener("kirimin", KeyEvent.KEYCODE_0, KeyEvent.ACTION_UP) isIgnore() initializeMocks() presenter.onUserIdEditTextKeyListener("kirimin", KeyEvent.KEYCODE_ENTER, KeyEvent.ACTION_DOWN) isIgnore() } presenter.onCreate() isSubmitButtonClick() withEditTextKeyEnter() } ,PUMJOͷϩʔΧϧؔ਺ʹΑͬͯ ߏ଄Խ͞Εͨςετέʔε
  20. @Test fun userIdSubmitTest() { whenever(useCaseMock.fetchUserInfo(anyString())).thenReturn(Single.never()) fun isError() { ... }

    fun isSuccess() { ... } fun isIgnore() { ... } fun isSubmitButtonClick() { initializeMocks() presenter.onSubmitButtonClick("") isError() initializeMocks() presenter.onSubmitButtonClick("kirimin") isSuccess() } fun withEditTextKeyEnter() { initializeMocks() presenter.onUserIdEditTextKeyListener("", KeyEvent.KEYCODE_ENTER, KeyEvent.ACTION_UP) isError() initializeMocks() presenter.onUserIdEditTextKeyListener("kirimin", KeyEvent.KEYCODE_ENTER, KeyEvent.ACTION_UP) isSuccess() initializeMocks() presenter.onUserIdEditTextKeyListener("kirimin", KeyEvent.KEYCODE_0, KeyEvent.ACTION_UP) isIgnore() initializeMocks() presenter.onUserIdEditTextKeyListener("kirimin", KeyEvent.KEYCODE_ENTER, KeyEvent.ACTION_DOWN) isIgnore() } presenter.onCreate() isSubmitButtonClick() withEditTextKeyEnter() } ϘλϯΛԡͯ͠ ϦΫΤετͨ͠৔߹ͷέʔε ΤϯλʔΩʔͰ ϦΫΤετͨ͠৔߹ͷέʔε ͦΕͧΕೖྗ஋ʹΑͬͯ Ͳ͏͍͏݁ՌʹͳΔ͔Λఆٛ
  21. @Test fun userIdSubmitTest() { whenever(useCaseMock.fetchUserInfo(anyString())).thenReturn(Single.never()) fun isError() { verify(viewMock, times(1)).showErrorToast(anyString())

    verify(viewMock, never()).setProgressBarVisibility(View.VISIBLE) verify(useCaseMock, never()).fetchUserInfo(anyString()) } fun isSuccess() { verify(viewMock, never()).showErrorToast(anyString()) verify(viewMock, times(1)).setParentLayoutVisibility(View.GONE) verify(viewMock, times(1)).setProgressBarVisibility(View.VISIBLE) verify(viewMock, never()).setParentLayoutVisibility(View.VISIBLE) verify(viewMock, never()).setProgressBarVisibility(View.GONE) verify(useCaseMock, times(1)).fetchUserInfo("kirimin") } fun isIgnore() { verify(viewMock, never()).showErrorToast(anyString()) verify(viewMock, never()).setProgressBarVisibility(anyInt()) verify(viewMock, never()).setParentLayoutVisibility(anyInt()) verify(useCaseMock, never()).fetchUserInfo(anyString()) } fun isSubmitButtonClick() { ... } fun withEditTextKeyEnter() { ... } presenter.onCreate() isSubmitButtonClick() withEditTextKeyEnter() }
  22. @Test fun onFetchUserInfoTest() { fun success() { fun hideProgressAndShowUserInfoLayout() {

    verify(viewMock, times(1)).setProgressBarVisibility(View.GONE) verify(viewMock, times(1)).setParentLayoutVisibility(View.VISIBLE) } fun setUserInfoMinCase() { val userEntity = UserEntity(name = "kirimin", location = null, company = null, blog = null, email = null, avatar_url = null) whenever(useCaseMock.fetchUserInfo("kirimin")).thenReturn(Single.just(User(userEntity, listOf(RepositoryEntity())))) initializeMocks() presenter.onSubmitButtonClick("kirimin") hideProgressAndShowUserInfoLayout() verify(viewMock, times(1)).setUserName("kirimin") verify(viewMock, times(1)).setLocationTextAndVisibility(eq(View.GONE), anyString()) verify(viewMock, times(1)).setMailTextAndVisibility(eq(View.GONE), anyString()) verify(viewMock, times(1)).setLinkTextAndVisibility(eq(View.GONE), anyString()) verify(viewMock, times(1)).setIconVisibility(View.INVISIBLE) } fun setUserInfoMaxCase() { val userEntity = UserEntity(name = "kirimin", location = "tokyo, japan", company = "kirimin inc.", blog = "http://kirimin.me", email = "cc@kirimin. avatar_url = "http://kirimin.me/face_icon.png") whenever(useCaseMock.fetchUserInfo("kirimin")).thenReturn(Single.just(User(userEntity, listOf(RepositoryEntity())))) initializeMocks() presenter.onSubmitButtonClick("kirimin") hideProgressAndShowUserInfoLayout() verify(viewMock, times(1)).setUserName("kirimin") verify(viewMock, times(1)).setLocationTextAndVisibility(View.VISIBLE, "tokyo, japan") verify(viewMock, times(1)).setMailTextAndVisibility(View.VISIBLE, "[email protected]") verify(viewMock, times(1)).setLinkTextAndVisibility(View.VISIBLE, "http://kirimin.me") verify(viewMock, times(1)).setIconVisibility(View.VISIBLE) verify(viewMock, times(1)).setIcon("http://kirimin.me/face_icon.png") } setUserInfoMinCase() setUserInfoMaxCase() } fun failed() { whenever(useCaseMock.fetchUserInfo("kirimin")).thenReturn(Single.error(Exception("the error"))) initializeMocks() presenter.onSubmitButtonClick("kirimin") verify(viewMock, times(1)).setProgressBarVisibility(View.GONE) verify(viewMock, never()).setParentLayoutVisibility(View.VISIBLE) verify(viewMock, times(1)).showErrorToast(anyString()) } presenter.onCreate() success() failed() }
  23. @Test fun onFetchUserInfoTest() { fun success() { fun hideProgressAndShowUserInfoLayout() {

    verify(viewMock, times(1)).setProgressBarVisibility(View.GONE) verify(viewMock, times(1)).setParentLayoutVisibility(View.VISIBLE) } fun setUserInfoMinCase() { val userEntity = UserEntity(name = "kirimin", location = null, company = null, blog = null, email = null, avatar_url = null) whenever(useCaseMock.fetchUserInfo("kirimin")).thenReturn(Single.just(User(userEntity, listOf(RepositoryEntity())))) initializeMocks() presenter.onSubmitButtonClick("kirimin") hideProgressAndShowUserInfoLayout() verify(viewMock, times(1)).setUserName("kirimin") ... } fun setUserInfoMaxCase() { val userEntity = UserEntity(name = "kirimin", location = "tokyo, japan", company = "kirimin inc.", blog = "http://kirimin.me = "[email protected]", avatar_url = "http://kirimin.me/face_icon.png") whenever(useCaseMock.fetchUserInfo("kirimin")).thenReturn(Single.just(User(userEntity, listOf(RepositoryEntity())))) initializeMocks() presenter.onSubmitButtonClick("kirimin") hideProgressAndShowUserInfoLayout() verify(viewMock, times(1)).setUserName("kirimin") ... } setUserInfoMinCase() setUserInfoMaxCase() } fun failed() { whenever(useCaseMock.fetchUserInfo("kirimin")).thenReturn(Single.error(Exception("the error"))) initializeMocks() presenter.onSubmitButtonClick("kirimin") verify(viewMock, times(1)).setProgressBarVisibility(View.GONE) ... } presenter.onCreate() success() failed() } ؔ਺ʹ੾Γग़ͨ͠ ڞ௨ͷςετέʔε औಘͨ͠σʔλ͝ͱͷ ςετέʔε
  24. private fun getUserInfo(id: String) { ... getLanguages(user.repositories).forEach { view.addLanguageView(it) }

    sortRepositories(user.repositories).forEach { view.addRepositoryView(it) } }, { view.setProgressBarVisibility(View.GONE) view.showErrorToast("network error") }).also { disposables.add(it) } } /** * ϦϙδτϦΛݴޠ͝ͱʹݴޠ໊ͱϦϙδτϦϦετͷPairʹ·ͱΊͨListΛฦ͢ */ private fun getLanguages(repositories: List<Repository>): List<Language> { return repositories .filter { it.language != null } .groupBy { it.language!! } .toList().sortedByDescending { language -> language.second.count() } .map { Language(it.first, it.second) } } /** * ϦϙδτϦΛελʔͷ਺ͱfork͔൱͔Ͱιʔτͯ͠ฦ͢ */ private fun sortRepositories(repositories: List<Repository>): List<Repository> { return repositories .sortedByDescending(Repository::starCount) .sortedBy(Repository::isFork) } 1SFTFOUFSʹॻ͔Εͨσʔλ ੔ܗͷQSJWBUFϝιου
  25. data class User(private val entity: UserEntity, private val repos: List<RepositoryEntity>)

    { val id = entity.id val name = entity.name val location = entity.location val mail = entity.email val link = entity.blog val iconUrl = entity.avatar_url val repositories = repos.map(::Repository) val languages = repositories .filter { it.language != null } .groupBy { it.language!! } .toList().sortedByDescending { language -> language.second.count() } .map { Language(it.first, it.second) } val sortedRepositories = repositories .sortedByDescending(Repository::starCount) .sortedBy(Repository::isFork) } υϝΠϯϞσϧ಺ͰॲཧΛߦ͍ ஋Λอ࣋͢ΔΑ͏ʹมߋ
  26. class UserTest { lateinit var user: User lateinit var a:

    RepositoryEntity lateinit var b: RepositoryEntity lateinit var c: RepositoryEntity lateinit var d: RepositoryEntity lateinit var e: RepositoryEntity @Before fun setup() { a = RepositoryEntity(name = "a", stargazers_count = 5, fork = false, language = "Java") b = RepositoryEntity(name = "b", stargazers_count = 2, fork = false, language = null) c = RepositoryEntity(name = "c", stargazers_count = 7, fork = true, language = "Kotlin") d = RepositoryEntity(name = "d", stargazers_count = 3, fork = false, language = "Kotlin") e = RepositoryEntity(name = "e", stargazers_count = 7, fork = false, language = "PHP") user = User(UserEntity(), listOf(a, b, c, d, e)) } @Test fun sortedRepositoriesTest() { user.sortedRepositories.should be listOf(e, a, d, b, c).map(::Repository) } @Test fun languagesTest() { user.languages.should be listOf( Language(name = "Kotlin", includeRepositories = listOf(c, d).map(::Repository)), Language(name = "Java", includeRepositories = listOf(a).map(::Repository)), Language(name = "PHP", includeRepositories = listOf(e).map(::Repository)) ) } } ςετ΋Ϟσϧʹର͢Δ ΑΓγϯϓϧͳϢχοτςετʹมߋ
  27. @Test fun addRepositoryViewTest() { val repositoryEntities = listOf( RepositoryEntity(name =

    "a"), RepositoryEntity(name = "b"), RepositoryEntity(name = "c") ) whenever(useCaseMock.fetchUserInfo("kirimin")).thenReturn(Single.just(User(UserEntity(name = "kirimin repositoryEntities))) presenter.onCreate() presenter.onSubmitButtonClick("kirimin") verify(viewMock, times(1)).addRepositoryView(Repository(RepositoryEntity(name = "a"))) verify(viewMock, times(1)).addRepositoryView(Repository(RepositoryEntity(name = "b"))) verify(viewMock, times(1)).addRepositoryView(Repository(RepositoryEntity(name = "c"))) } @Test fun addLanguageViewTest() { val a = mock<Language>() val b = mock<Language>() val c = mock<Language> { on { name } doReturn "c" } val languagesMock = listOf(a, b, c) val userMock = mock<User> { on { languages } doReturn languagesMock } whenever(useCaseMock.fetchUserInfo("kirimin")).thenReturn(Single.just(userMock)) presenter.onCreate() presenter.onSubmitButtonClick("kirimin") verify(viewMock, times(1)).addLanguageView(a) verify(viewMock, times(1)).addLanguageView(b) verify(viewMock, times(1)).addLanguageView(c) } 1SFTFOUFSଆͷςετ͸ .PDLΛ࢖༻ͨ͠؆୯ͳ΋ͷ͚ͩʹ