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

How to improve your MVP architecture and tests

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest. →

How to improve your MVP architecture and tests

for DroidKaigi2018

Avatar for きりみん

きりみん

February 07, 2018

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Λ࢖༻ͨ͠؆୯ͳ΋ͷ͚ͩʹ