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

D96c7e61d2f394f1d0af66945181a230?s=128

きりみん

February 07, 2018
Tweet

Transcript

  1. How to improve your MVP architecture and tests DroidKaigi 2018

    @kirimin
  2. About me

  3. About me • kiriminͱ͍͏IDͰTwitter΍GitHub΍ͬͯ·͢ • ΈΜ͔Β͖Γ·Ͱͱ͍͏ϒϩάͰ৭ʑॻ͍ͯ·͢ • 3೥൒͘Β͍ϑϦʔͰAndroidΤϯδχΞ΍ͬͯ ·ͨ͠ •

    ࠓ͸AnyPayͱ͍͏ձࣾͰpaymoͱ͍͏ΞϓϦΛ ࡞ͬͯ·͢
  4. ࠓ೔࿩͢͜ͱ • MVPΞʔΩςΫνϟ࠾༻ϓϩδΣΫτͷɹɹɹɹ Ξϯνύλʔϯ঺հ • Ξϯνύλʔϯͷվળํ๏ͱɺPresenterʹର͢Δ ςετͷॻ͖ํ΍໾ׂͷղઆ

  5. None
  6. About paymo • εϚϗͰׂΓצͷਫ਼ࢉ͕ ग़དྷΔܾࡁΞϓϦ • ҿΈձ΍ϥϯνͳͲͰҰ ׅձܭɺޙ͔ΒϢʔβʔ ಉ࢜Ͱ੥ٻͨ͠Γࢧ෷ͬ ͨΓग़དྷΔ

    • QRίʔυܾࡁػೳͳͲ ΋͋Γ·͢
  7. About paymo • 90%Kotlin • MVP Architecture • Unit Test

    for Presenter • CI with Bitrise
  8. paymo's Architecture %BUB૚ %PNBJO૚ 1SFTFOUBUJPO૚ 7JFX 1SFTFOUFS 6TF$BTF 3FQPTJUPSZ %BUB4UPSF

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

  10. Model View Presenter

  11. MVPɺ΍ͬͯ·͔͢ʁ • Androidք۾Ͱ2೥͘Β͍લʹ࿩୊ʹͳͬͨ • Controllerͷ୅ΘΓʹPresenterͱ͍͏UIϩδοΫΛ୲͏Ϋ ϥεΛ༻ҙ͢Δ • Activity΍Fragment͸Viewͱͯ͠ѻ͍ɺۃྗϩδοΫΛ࣋ ͨͤͳ͍ •

    Presenterͱ͍͏֓೦͸Clean ArchitectureͰ΋Ͱͯ͘ΔͨΊ ࠾༻͍ͯ͠ΔϓϩδΣΫτ΋݁ߏݟ͔͚Δ
  12. MVPͷ֓ཁ 7JFX 6*ඳը 1SFTFOUFS 6*ϩδοΫ .PEFM ϏδωεϩδοΫ Πϕϯτ௨஌ ඳըࢦࣔ

  13. Crean Architecture "OESPJE$MFBO"SDIJUFDUVSF $PQZSJHIU'FSOBOEP$FKBT -JDFOTFEVOEFSUIF"QBDIF-JDFOTF 7FSTJPO

  14. Α͘ݴΘΕΔMVPͷϝϦοτ • View͔ΒPresenterʹUIϩδοΫΛ੾Γग़͢ࣄͰɺ ςετ͕ॻ͖΍͘͢ͳΔ • ໾ׂͷ໌֬ԽʹΑΔίʔυͷՄಡੑɺ඼࣭ͷ޲্ • ઃܭ͕ܾ·͍ͬͯΔࣄʹΑΔ࣮૷ɺϨϏϡʔίε τͷݮগ •

    ActivityͷංେԽΛ๷ࢭ
  15. ͍͍ࣄͣ͘Ί͡ΌΜʂʂʂ ࠓ͙͢ಋೖ͠Α͏ͥʂʂʂ

  16. None
  17. MVP࠾༻ϓϩδΣΫτ Ξϯνύλʔϯ ࣮ࡍʹ͍͔ͭ͘ͷ ϓϩδΣΫτͰܦݧͨ͠

  18. ̍ɽςετ͕ॻ͔Ε͍ͯͳ͍

  19. ̎ɽPresenter͕ Activity΍FragmentΛ ௚઀ࢀর͠ૢ࡞ͯ͠Δ

  20. ̏ɽViewʹϩδοΫ͕ॻ͔Εͯ ͍ͨΓɺPresenter͕UIΛ ௚઀ૢ࡞͍ͯͨ͠ΓҰ؏ੑ͕ͳ͍

  21. ʮ͋ΕɺMVPͬͯView͕ ෼྾ͯ͠ίʔυ͕௥͍ʹ͘͘ ͳ͚ͬͨͩ͡Όͳ͍ʁʯ

  22. Ͳ͏ͯ͜͠Μͳ͜ͱʹ…

  23. ݪҼ ViewͱPresenterͷ੹຿෼͚͕ग़དྷ͍ͯͳ͍

  24. Ξϯνύλʔϯͷ վળҊ

  25. MVPΞϯνύλʔϯվળ ΞδΣϯμ • ViewͱPresenterΛ෼཭͢Δ • Presenterʹର͢ΔςετΛॻ͘ • ςετΛϦϑΝΫλϦϯά͢Δ

  26. MVPΞϯνύλʔϯվળ ΞδΣϯμ • ViewͱPresenterΛ෼཭͢Δ • Presenterʹର͢ΔςετΛॻ͘ • ςετΛϦϑΝΫλϦϯά͢Δ

  27. ViewͱPresenterΛ෼཭͢Δ • ੹຿ΛܾΊͯ΋Activity΍FragmentΛ௚઀Presenter ͕ࢀরग़དྷͯ͠·͏ͱɺ݁ہͳΜͰ΋ग़དྷͯ͠·͏ • Presenterʹ௚઀Androidʹґଘͨ͠Viewૢ࡞΍σʔ λΞΫηεॲཧ͕ॻ͔ΕͯΔͱɺϞοΫԽ͕ग़དྷͣς ετΛॻ͘ͷ͕೉͘͠ͳΔ • ·ͣ͸ViewΛந৅Խͯ͠ڧ੍తʹ੹຿Λ෼཭͠Α͏ʂ

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

    ViewͱPresenterΛ෼཭͢Δ • Androidʹґଘͨ͠ॲཧ΍σʔλΞΫηεॲཧΛ Presenter͔Β෼཭͠ɺPresenterΛϐϡΞʹอͭ
  29. 1SFTFOUFS 7JFX .PDL 6TF$BTF .PDL ViewͱPresenterΛ෼཭͢Δ • ςετ࣌ʹ͸෼཭ͨ͠෦෼ΛMockԽ͢Δ͜ͱͰɺ Presenterʹςετ͕ॻ͖΍͘͢ͳΔ

  30. αϯϓϧίʔυ

  31. αϯϓϧΞϓϦ • GitHubΫϥΠΞϯτΞϓϦ • ೖྗͨ͠IDͷϢʔβʔͷ ProfileΛදࣔ͢Δ͚ͩ • γϯάϧϖʔδͷγϯϓϧ ͳΞϓϦ •

    ࿩Λ෼͔Γ΍͘͢͢ΔͨΊ ʹDaggerͳͲ͸ະ࢖༻
  32. ·ͣ͸Ξϯνύλʔϯ

  33. View(Activity)

  34. 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) } ...
  35. 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ʹΠϕϯτΛ௨஌ ͢ΔͷͰ͸ͳ͘ɺࢦࣔΛग़͍ͯ͠Δ
  36. Presenter

  37. 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 } }
  38. 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ଆͰߦ͍ͬͯΔ
  39. վળޙ

  40. View(Activity)

  41. 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ʹ௨஌͢ΔͷΈ
  42. ... 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Λ௚઀ૢ࡞͢Δ ୹͍ϝιου܈Λ࣋ͭ
  43. } 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͔ΒΞΫηεՄೳ
  44. Presenter

  45. 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͸ΠϕϯτʹԠͨ͡ॲཧΛߦ͏
  46. 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ʹ σʔλͷऔಘΛґཔ
  47. ·ͱΊ • ViewΛந৅Խ͢Δ͜ͱͰPresenter͸༧ΊΠϯλʔ ϑΣΠεͰఆٛͨ͠ϝιουܦ༝Ͱ͔͠ViewʹΞΫη εग़དྷͳΓڧ੍ྗ͕ಇ͘ • View͸Πϕϯτ௨஌ͱඳըͷΈΛߦ͍ɺදग़൑ఆͳͲ ͷϩδοΫ͸PresenterଆͰߦ͏

  48. MVPΞϯνύλʔϯվળ ΞδΣϯμ • ViewͱPresenterΛ෼཭͢Δ • Presenterʹର͢ΔςετΛॻ͘ • ςετΛϦϑΝΫλϦϯά͢Δ

  49. ԿނPresenterʹςετΛॻ͔͘ • [ॳΊͯͷࣗಈςετ web γεςϜͷͨΊͷࣗಈςε τೖ໳]Λࢀߟʹߟ͑ͯΈΔ • Webͷςετʹ͍ͭͯॻ͔ Εͨຊ͕ͩɺࣗಈςετ΁ ͷߟ͑ํͳͲͱͯ΋ࢀߟʹ

    ͳΔҰ࡭
  50. ςετͷ෼ྨ • ςετʹ͸େ͖͘Θ͚ͯUIς ετɺ౷߹ςετɺϢχοτ ςετͷ3͕ͭ͋Δ • 3ͭ͸ϐϥϛοτͷؔ܎Ͱɺ ্ʹߦ͘΄Ͳ࣮૷ɾ࣮ߦίε τ͕ߴ͍͕ຊ൪؀ڥʹ͍ۙ UIςετ

    ౷߹ςετ Ϣχοτςετ by ॳΊͯͷࣗಈςετ webγεςϜͷͨΊͷࣗಈςετೖ໳
  51. Presenter΁ͷςετ • Presenter΁ͷςετ͸͜ͷ தͰ͸౷߹ςετʹ౰ͨΔ • ϢχοτςετΑΓ΋޿͍ ൣғΛςετग़དྷͯɺUIς ετΑΓ΋ίετ͕௿͍ UIςετ ౷߹ςετ

    Ϣχοτςετ
  52. PresenterʹςετΛॻ͘ ϝϦοτ • Viewͷදग़൑ఆͳͲͷը໘શମͷϩδοΫͷਖ਼͠͞ΛUI ςετʹཻ͍ۙ౓ͰνΣοΫͰ͖Δ • ޿͍ൣғͰͷσάϨΛݕ஌͠΍͘͢ͳΔ • PresenterʹϩδοΫΛدͤςετΛॻ͘ࣄͰɺPresenter ಺ͷॲཧͷϦϑΝΫλ΍ϞσϧΫϥε΁ͷ੾Γग़͠ͳͲ͕

    ΍Γ΍͘͢ͳΔ • ϩʔΧϧϢχοτςετͱͯ͠ΤϛϡϨʔλʔແ͠Ͱૉૣ ࣮͘ߦग़དྷΔͨΊɺؾܰʹ࣮ߦͯ֬͠ೝग़དྷΔ
  53. Presenter΁ͷςετͷॻ͖ํ • ϞοΫϥΠϒϥϦͰView΍UseCaseΛMockԽ • PresenterͷΠϕϯτϝιουΛݺͼɺॲཧͷ݁Ռ View΍UseCaseͷϝιου͕ظ଴௨Γʹݺ͹Ε͍ͯ Δ͔ΛνΣοΫ͢ΔࣄͰςετΛߦ͏

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

    ͓͞Β͍ • Androidʹґଘͨ͠ॲཧ΍σʔλΞΫηεॲཧΛ Presenter͔Β෼཭͠ɺPresenterΛϐϡΞʹอͭ
  55. 1SFTFOUFS 7JFX .PDL 6TF$BTF .PDL ͓͞Β͍ • ςετ࣌ʹ͸෼཭ͨ͠෦෼ΛMockԽ͢Δ͜ͱͰɺ Presenterʹςετ͕ॻ͖΍͘͢ͳΔ

  56. MockԽ͢Δͬͯ۩ମతʹ Ͳ͏͍͏͜ͱʁ

  57. MockitoΛ࢖͏ • JavaͷϝδϟʔͳϞοΫϥΠϒϥϦ • Ϋϥε΍ΠϯλʔϑΣΠεͷϞοΫΠϯελϯεΛੜ ੒͠ɺϝιουʹ೚ҙͷ໭Γ஋Λࢦఆͨ͠Γɺϝιο υ͕ݺ͹Εͨճ਺Λ֬ೝͨ͠Γग़དྷΔ // TopViewͷϞοΫΠϯελϯεΛੜ੒ viewMock

    = mock(TopView::class.java) // presenterͷonCreate()ϝιουΛςετ಺ͰݺͿ presenter = TopPresenter(viewMock, useCaseMock) presenter.onCreate() // ݁ՌviewMockͷinitView()ϝιου͕ݺ͹Ε͍ͯΔࣄΛ֬ೝ verify(viewMock, Mockito.times(1)).initView()
  58. αϯϓϧίʔυ

  59. 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) }
  60. @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") }
  61. @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 = "cc@kirimin.me", 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, "cc@kirimin.me") 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") }
  62. ·ͱΊ • Presenter͔ΒAndroidϑϨʔϜϫʔΫʹґଘͨ͠ॲཧ΍ σʔλΞΫηεॲཧΛ෼཭͢Δ͜ͱͰɺPresenterʹରͯ͠ ϩʔΧϧϢχοτςετΛهड़Ͱ͖Δ • Presenterʹର͢Δςετ͸શମͷॲཧͷਖ਼͠͞Λݕূ͢Δ ౷߹ςετͱ͍͏Ґஔ͚ͮ • MockitoͷverifyϝιουͳͲΛ׆༻ͯ͠ɺPresenterͷॲ

    ཧͷ݁ՌɺView΍UseCaseͷϝιου͕ظ଴௨Γʹݺ͹Ε ͍ͯΔ͔Λݕূ͢Δ
  63. ͓ΊͰͱ͏ʂʂʂ ͜ΕͰPresenterʹςετ͕ॻ ͚ΔΑ͏ʹͳͬͨͧʂ

  64. 1೥ޙ...

  65. None
  66. ࣮ࡍʹϓϩμΫτͰPresenterʹ ςετΛॻ͖࢝Ίͯײͨ͡೉఺ • ҙຯͷ͋Δཻ౓ͷςετΛॻ͘ʹ͸ͦΕͳΓʹίετ ֻ͕͔Δɻ࣍ୈʹ໘౗ष͕͞ڧ͘ͳΓࡶʹͳΔ • ৽ن࣮૷࣌͸ஸೡʹςετΛॻ͕͘ɺվम࣌͸Ͳ͏͠ ͯ΋ࡶʹམͪͨςετΛ௚͚ͩ͢Ͱຬ଍ͯ͠͠·͏ • ෳࡶͳը໘ͩͱςετίʔυ͕ͲΜͲΜංେԽͯ͠͠

    ·͏ɻਂ͘ߟ͑ͣʹॻ͘ͱ৑௕Ͱ͋·Γҙຯͷͳ͍ς ετίʔυʹͳͬͯ͠·͏
  67. MVPΞϯνύλʔϯվળ ΞδΣϯμ • ViewͱPresenterΛ෼཭͢Δ • Presenterʹର͢ΔςετΛॻ͘ • ςετίʔυΛϦϑΝΫλϦϯά͢Δ

  68. ςετίʔυΛϦϑΝΫλ͢Δ • ॏෳͨ͠ςετίʔυ͕͋Δͱ࢓༷มߋ࣌ͷมߋՕॴ ͕૿͑Δ • ίʔυྔ΋૿͑ΔͷͰॻ͘ͷ΋ಡΉͷ΋ਏ͘ͳΓ಺༰ ͕ࡶʹͳΔ • ίετͷߴ͍ςετ͸ॻ͔ͳ͘ͳΔ

  69. ςετίʔυΛ ΋͏Ұ౓ݟͯΈΔ

  70. @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 = "cc@kirimin.me", 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, "cc@kirimin.me") 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") } σʔλͷύλʔϯ͝ͱʹ ςετϝιουΛॻ͍͍ͯΔ ͦΕͧΕͷςετͰ ॏෳίʔυΛॻ͍ͯΔ σʔλύλʔϯ΍ ঢ়ଶ͕૿͑Δ΄Ͳ ςετίʔυ͕ංେԽ͢Δ
  71. Ͳ͏ͨ͠Β͍͍ͩΖ͏ • ग़དྷΔ͚ͩޮ཰తͰมߋʹ΋ڧ͍ςετίʔυʹͨ͠ ͍ • ڞ௨ͷ෦෼͸ಉ͡ςετΛ௨ͯ͠ɺέʔε͝ͱͷࠩ෼ ͷΈνΣοΫ͍ͨ͠

  72. ςετίʔυΛߏ଄Խͯ͠ΈΔ • Kotlin༻ςετϑϨʔϜϫʔΫϥΠϒϥϦͷSpekͳͲ Λࢀߟʹͯ͠ɺߏ଄Խͨ͠ςετΛߟ͑Δ • SpekͳͲͷϥΠϒϥϦΛར༻ͯ͠΋ྑ͍͕ɺSpek͸ େܕΞοϓσʔτ͕߇͍͑ͯͯ࣌ظ͕ѱ͍ࣄͳͲ΋͋ Γɺࠓճ͸Kotlinͷඪ४ػೳͷϩʔΧϧؔ਺Λ࢖ͬͯ ࣮૷ͯ͠ΈΔ

  73. @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ͷϩʔΧϧؔ਺ʹΑͬͯ ߏ଄Խ͞Εͨςετέʔε
  74. @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() } ϘλϯΛԡͯ͠ ϦΫΤετͨ͠৔߹ͷέʔε ΤϯλʔΩʔͰ ϦΫΤετͨ͠৔߹ͷέʔε ͦΕͧΕೖྗ஋ʹΑͬͯ Ͳ͏͍͏݁ՌʹͳΔ͔Λఆٛ
  75. @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() }
  76. @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, "cc@kirimin.me") 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() }
  77. @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 = "cc@kirimin.me", 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() } ؔ਺ʹ੾Γग़ͨ͠ ڞ௨ͷςετέʔε औಘͨ͠σʔλ͝ͱͷ ςετέʔε
  78. ·ͱΊ • KotlinͷϩʔΧϧؔ਺Λར༻ͯ͠ςετίʔυΛߏ଄ Խͯ͠ΈΔࢼΈΛ΍͍ͬͯ·͢ • ಡΈʹ͘͘ͳΓ͕ͪͳMockitoΛར༻ͨ͠ςετΛ੔ ཧͯ͠ՄಡੑΛ্͛Δ • ݕূ͍ͨ͠ύλʔϯ΍έʔε͕૿͑ͯ΋ίʔυ͕ංେ Խͨ͠Γमਖ਼ίετ͕૿େ͠ͳ͍Α͏ʹ͢Δ

  79. PresenterͷॲཧΛ υϝΠϯϞσϧʹҠৡ͢Δ

  80. ςετͷϐϥϛουΛࢥ͍ग़͢ • ౷߹ςετ͸ڧྗͰศར͕ͩɺ Ϟσϧʹର͢ΔγϯϓϧͳΞ αʔγϣϯςετ͸ΑΓॻ͖ ΍͘͢ਫ਼౓΋ߴ͍ • ຊདྷ͸Ϣχοτςετͷൺॏ ͕େ͖͍ͷ͕݈શ •

    Presenter͔ΒϞσϧʹॲཧ ͱςετΛҠ͍ͯ͜͠͏ UIςετ ౷߹ςετ Ϣχοτςετ
  81. 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ϝιου
  82. 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) } υϝΠϯϞσϧ಺ͰॲཧΛߦ͍ ஋Λอ࣋͢ΔΑ͏ʹมߋ
  83. 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)) ) } } ςετ΋Ϟσϧʹର͢Δ ΑΓγϯϓϧͳϢχοτςετʹมߋ
  84. @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Λ࢖༻ͨ͠؆୯ͳ΋ͷ͚ͩʹ
  85. ·ͱΊ • MockitoΛ࢖ͬͨ౷߹ςετΑΓΞαʔγϣϯʹΑΔϢχο τςετͷํ͕ίετ͕௿͘ਫ਼౓΋ߴ͍ • Presenter͔ΒυϝΠϯϞσϧͱͯ͠੾Γग़ͤΔॲཧΛݟ͚ͭ Α͏ • গͣͭ͠ϢχοτςετΛ૿΍͍ͯ͘͜͠ͱͰɺPresenterʹ ର͢Δςετ͕ංେԽ͠ͳ͍Α͏ʹ͢Δ

    • ຊ౰ʹΫϦςΟΧϧͳ෦෼ͷςετ͸EspressoͳͲΛར༻͠ ͨUIςετΛ࢖͏બ୒΋͋Δ
  86. ”UIςετΑΓϢχοτςετΛ ༏ઌ͠Α͏ɻϢχοτςετͰຒ ΊΒΕͳ͍伱ؒΛ౷߹ςετͰΧ όʔ͠Α͏ɻ” by ॳΊͯͷࣗಈςετ webγεςϜͷͨΊͷࣗಈςετೖ໳

  87. ͍͞͝ʹ • ઃܭ΋ςετ΋·ͩ·ͩࢼߦࡨޡͰ΍͍ͬͯ·͢ • ࠓճͷ಺༰΋ݸਓͷݟղͳͷͰࢍ൱͋Δͱࢥ͍·͢ • ͖Ε͍ͳςετͷॻ͖ํͳͲͥͻڭ͍͑ͯͩ͘͞ʂ • ࠓ೔࿩͖͠Εͳ͔ͬͨ಺༰͸·ͨͲ͔͜Ͱඞͣʂ

  88. ͝ਗ਼ௌ ͋Γ͕ͱ͏͍͟͝·ͨ͠