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. How to improve your
    MVP architecture
    and tests
    DroidKaigi 2018
    @kirimin

    View Slide

  2. About me

    View Slide

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

    View Slide

  4. ࠓ೔࿩͢͜ͱ
    • MVPΞʔΩςΫνϟ࠾༻ϓϩδΣΫτͷɹɹɹɹ
    Ξϯνύλʔϯ঺հ
    • Ξϯνύλʔϯͷվળํ๏ͱɺPresenterʹର͢Δ
    ςετͷॻ͖ํ΍໾ׂͷղઆ

    View Slide

  5. View Slide

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

    View Slide

  7. About paymo
    • 90%Kotlin
    • MVP Architecture
    • Unit Test for Presenter
    • CI with Bitrise

    View Slide

  8. paymo's Architecture
    %BUB૚
    %PNBJO૚
    1SFTFOUBUJPO૚
    7JFX
    1SFTFOUFS
    6TF$BTF
    3FQPTJUPSZ
    %BUB4UPSF
    "1* %# 1SFGFSFODFT
    "DUJWJUZ
    'SBHNFOU
    $VTUPN7JFX
    3FQPTJUPSZ
    %BUB4UPSF
    3FQPTJUPSZ
    %BUB4UPSF
    .PEFM
    %*
    %*
    %*

    View Slide

  9. MVP

    View Slide

  10. Model
    View
    Presenter

    View Slide

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

    View Slide

  12. MVPͷ֓ཁ
    7JFX
    6*ඳը
    1SFTFOUFS
    6*ϩδοΫ
    .PEFM
    ϏδωεϩδοΫ
    Πϕϯτ௨஌
    ඳըࢦࣔ

    View Slide

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

    View Slide

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

    View Slide

  15. ͍͍ࣄͣ͘Ί͡ΌΜʂʂʂ
    ࠓ͙͢ಋೖ͠Α͏ͥʂʂʂ

    View Slide

  16. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  28. 1SFTFOUFS
    7JFX 6TF$BTF
    ඳը
    "OESPJE
    '8
    %#
    "1*
    4IBSFE
    1SFGFSFODFT
    ViewͱPresenterΛ෼཭͢Δ
    • Androidʹґଘͨ͠ॲཧ΍σʔλΞΫηεॲཧΛ
    Presenter͔Β෼཭͠ɺPresenterΛϐϡΞʹอͭ

    View Slide

  29. 1SFTFOUFS
    7JFX
    .PDL
    6TF$BTF
    .PDL
    ViewͱPresenterΛ෼཭͢Δ
    • ςετ࣌ʹ͸෼཭ͨ͠෦෼ΛMockԽ͢Δ͜ͱͰɺ
    Presenterʹςετ͕ॻ͖΍͘͢ͳΔ

    View Slide

  30. αϯϓϧίʔυ

    View Slide

  31. αϯϓϧΞϓϦ
    • GitHubΫϥΠΞϯτΞϓϦ
    • ೖྗͨ͠IDͷϢʔβʔͷ
    ProfileΛදࣔ͢Δ͚ͩ
    • γϯάϧϖʔδͷγϯϓϧ
    ͳΞϓϦ
    • ࿩Λ෼͔Γ΍͘͢͢ΔͨΊ
    ʹDaggerͳͲ͸ະ࢖༻

    View Slide

  32. ·ͣ͸Ξϯνύλʔϯ

    View Slide

  33. View(Activity)

    View Slide

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

    View Slide

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

    View Slide

  36. Presenter

    View Slide

  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(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): List>> {
    return repositories
    .filter { it.language != null }
    .groupBy { it.language!! }
    .toList().sortedByDescending { language -> language.second.count() }
    }
    /**
    * ϦϙδτϦΛελʔͷ਺ͱfork͔൱͔Ͱιʔτͯ͠ฦ͢
    */
    fun sortRepositories(repositories: List): List {
    return repositories
    .sortedByDescending { repo -> repo.starCount }
    .sortedBy { repo -> repo.isFork }
    }

    View Slide

  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(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ଆͰߦ͍ͬͯΔ

    View Slide

  39. վળޙ

    View Slide

  40. View(Activity)

    View Slide

  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ʹ௨஌͢ΔͷΈ

    View Slide

  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Λ௚઀ૢ࡞͢Δ
    ୹͍ϝιου܈Λ࣋ͭ

    View Slide

  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͔ΒΞΫηεՄೳ

    View Slide

  44. Presenter

    View Slide

  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͸ΠϕϯτʹԠͨ͡ॲཧΛߦ͏

    View Slide

  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ʹ
    σʔλͷऔಘΛґཔ

    View Slide

  47. ·ͱΊ
    • ViewΛந৅Խ͢Δ͜ͱͰPresenter͸༧ΊΠϯλʔ
    ϑΣΠεͰఆٛͨ͠ϝιουܦ༝Ͱ͔͠ViewʹΞΫη
    εग़དྷͳΓڧ੍ྗ͕ಇ͘
    • View͸Πϕϯτ௨஌ͱඳըͷΈΛߦ͍ɺදग़൑ఆͳͲ
    ͷϩδοΫ͸PresenterଆͰߦ͏

    View Slide

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

    View Slide

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

    View Slide

  50. ςετͷ෼ྨ
    • ςετʹ͸େ͖͘Θ͚ͯUIς
    ετɺ౷߹ςετɺϢχοτ
    ςετͷ3͕ͭ͋Δ
    • 3ͭ͸ϐϥϛοτͷؔ܎Ͱɺ
    ্ʹߦ͘΄Ͳ࣮૷ɾ࣮ߦίε
    τ͕ߴ͍͕ຊ൪؀ڥʹ͍ۙ
    UIςετ
    ౷߹ςετ
    Ϣχοτςετ
    by ॳΊͯͷࣗಈςετ webγεςϜͷͨΊͷࣗಈςετೖ໳

    View Slide

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

    View Slide

  52. PresenterʹςετΛॻ͘
    ϝϦοτ
    • Viewͷදग़൑ఆͳͲͷը໘શମͷϩδοΫͷਖ਼͠͞ΛUI
    ςετʹཻ͍ۙ౓ͰνΣοΫͰ͖Δ
    • ޿͍ൣғͰͷσάϨΛݕ஌͠΍͘͢ͳΔ
    • PresenterʹϩδοΫΛدͤςετΛॻ͘ࣄͰɺPresenter
    ಺ͷॲཧͷϦϑΝΫλ΍ϞσϧΫϥε΁ͷ੾Γग़͠ͳͲ͕
    ΍Γ΍͘͢ͳΔ
    • ϩʔΧϧϢχοτςετͱͯ͠ΤϛϡϨʔλʔແ͠Ͱૉૣ
    ࣮͘ߦग़དྷΔͨΊɺؾܰʹ࣮ߦͯ֬͠ೝग़དྷΔ

    View Slide

  53. Presenter΁ͷςετͷॻ͖ํ
    • ϞοΫϥΠϒϥϦͰView΍UseCaseΛMockԽ
    • PresenterͷΠϕϯτϝιουΛݺͼɺॲཧͷ݁Ռ
    View΍UseCaseͷϝιου͕ظ଴௨Γʹݺ͹Ε͍ͯ
    Δ͔ΛνΣοΫ͢ΔࣄͰςετΛߦ͏

    View Slide

  54. 1SFTFOUFS
    7JFX 6TF$BTF
    ඳը
    "OESPJE
    '8
    %#
    "1*
    4IBSFE
    1SFGFSFODFT
    ͓͞Β͍
    • Androidʹґଘͨ͠ॲཧ΍σʔλΞΫηεॲཧΛ
    Presenter͔Β෼཭͠ɺPresenterΛϐϡΞʹอͭ

    View Slide

  55. 1SFTFOUFS
    7JFX
    .PDL
    6TF$BTF
    .PDL
    ͓͞Β͍
    • ςετ࣌ʹ͸෼཭ͨ͠෦෼ΛMockԽ͢Δ͜ͱͰɺ
    Presenterʹςετ͕ॻ͖΍͘͢ͳΔ

    View Slide

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

    View Slide

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

    View Slide

  58. αϯϓϧίʔυ

    View Slide

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

    View Slide

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

    View Slide

  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 = "[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")
    }

    View Slide

  62. ·ͱΊ
    • Presenter͔ΒAndroidϑϨʔϜϫʔΫʹґଘͨ͠ॲཧ΍
    σʔλΞΫηεॲཧΛ෼཭͢Δ͜ͱͰɺPresenterʹରͯ͠
    ϩʔΧϧϢχοτςετΛهड़Ͱ͖Δ
    • Presenterʹର͢Δςετ͸શମͷॲཧͷਖ਼͠͞Λݕূ͢Δ
    ౷߹ςετͱ͍͏Ґஔ͚ͮ
    • MockitoͷverifyϝιουͳͲΛ׆༻ͯ͠ɺPresenterͷॲ
    ཧͷ݁ՌɺView΍UseCaseͷϝιου͕ظ଴௨Γʹݺ͹Ε
    ͍ͯΔ͔Λݕূ͢Δ

    View Slide

  63. ͓ΊͰͱ͏ʂʂʂ
    ͜ΕͰPresenterʹςετ͕ॻ
    ͚ΔΑ͏ʹͳͬͨͧʂ

    View Slide

  64. 1೥ޙ...

    View Slide

  65. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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 = "[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")
    }
    σʔλͷύλʔϯ͝ͱʹ
    ςετϝιουΛॻ͍͍ͯΔ
    ͦΕͧΕͷςετͰ
    ॏෳίʔυΛॻ͍ͯΔ
    σʔλύλʔϯ΍
    ঢ়ଶ͕૿͑Δ΄Ͳ
    ςετίʔυ͕ංେԽ͢Δ

    View Slide

  71. Ͳ͏ͨ͠Β͍͍ͩΖ͏
    • ग़དྷΔ͚ͩޮ཰తͰมߋʹ΋ڧ͍ςετίʔυʹͨ͠
    ͍
    • ڞ௨ͷ෦෼͸ಉ͡ςετΛ௨ͯ͠ɺέʔε͝ͱͷࠩ෼
    ͷΈνΣοΫ͍ͨ͠

    View Slide

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

    View Slide

  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ͷϩʔΧϧؔ਺ʹΑͬͯ
    ߏ଄Խ͞Εͨςετέʔε

    View Slide

  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()
    }
    ϘλϯΛԡͯ͠
    ϦΫΤετͨ͠৔߹ͷέʔε
    ΤϯλʔΩʔͰ
    ϦΫΤετͨ͠৔߹ͷέʔε
    ͦΕͧΕೖྗ஋ʹΑͬͯ
    Ͳ͏͍͏݁ՌʹͳΔ͔Λఆٛ

    View Slide

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

    View Slide

  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 = "[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")
    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()
    }

    View Slide

  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
    = "[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()
    }
    ؔ਺ʹ੾Γग़ͨ͠
    ڞ௨ͷςετέʔε
    औಘͨ͠σʔλ͝ͱͷ
    ςετέʔε

    View Slide

  78. ·ͱΊ
    • KotlinͷϩʔΧϧؔ਺Λར༻ͯ͠ςετίʔυΛߏ଄
    Խͯ͠ΈΔࢼΈΛ΍͍ͬͯ·͢
    • ಡΈʹ͘͘ͳΓ͕ͪͳMockitoΛར༻ͨ͠ςετΛ੔
    ཧͯ͠ՄಡੑΛ্͛Δ
    • ݕূ͍ͨ͠ύλʔϯ΍έʔε͕૿͑ͯ΋ίʔυ͕ංେ
    Խͨ͠Γमਖ਼ίετ͕૿େ͠ͳ͍Α͏ʹ͢Δ

    View Slide

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

    View Slide

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

    View Slide

  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): List {
    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): List {
    return repositories
    .sortedByDescending(Repository::starCount)
    .sortedBy(Repository::isFork)
    }
    1SFTFOUFSʹॻ͔Εͨσʔλ
    ੔ܗͷQSJWBUFϝιου

    View Slide

  82. data class User(private val entity: UserEntity, private val repos:
    List) {
    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)
    }
    υϝΠϯϞσϧ಺ͰॲཧΛߦ͍
    ஋Λอ࣋͢ΔΑ͏ʹมߋ

    View Slide

  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))
    )
    }
    }
    ςετ΋Ϟσϧʹର͢Δ
    ΑΓγϯϓϧͳϢχοτςετʹมߋ

    View Slide

  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()
    val b = mock()
    val c = mock { on { name } doReturn "c" }
    val languagesMock = listOf(a, b, c)
    val userMock = mock { 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Λ࢖༻ͨ͠؆୯ͳ΋ͷ͚ͩʹ

    View Slide

  85. ·ͱΊ
    • MockitoΛ࢖ͬͨ౷߹ςετΑΓΞαʔγϣϯʹΑΔϢχο
    τςετͷํ͕ίετ͕௿͘ਫ਼౓΋ߴ͍
    • Presenter͔ΒυϝΠϯϞσϧͱͯ͠੾Γग़ͤΔॲཧΛݟ͚ͭ
    Α͏
    • গͣͭ͠ϢχοτςετΛ૿΍͍ͯ͘͜͠ͱͰɺPresenterʹ
    ର͢Δςετ͕ංେԽ͠ͳ͍Α͏ʹ͢Δ
    • ຊ౰ʹΫϦςΟΧϧͳ෦෼ͷςετ͸EspressoͳͲΛར༻͠
    ͨUIςετΛ࢖͏બ୒΋͋Δ

    View Slide

  86. ”UIςετΑΓϢχοτςετΛ
    ༏ઌ͠Α͏ɻϢχοτςετͰຒ
    ΊΒΕͳ͍伱ؒΛ౷߹ςετͰΧ
    όʔ͠Α͏ɻ”
    by ॳΊͯͷࣗಈςετ webγεςϜͷͨΊͷࣗಈςετೖ໳

    View Slide

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

    View Slide

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

    View Slide