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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  7. Model
    View
    Presenter

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  26. αϯϓϧίʔυ

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  29. View(Activity)

    View full-size slide

  30. 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 full-size slide

  31. 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 full-size slide

  32. 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 full-size slide

  33. 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 full-size slide

  34. View(Activity)

    View full-size slide

  35. 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 full-size slide

  36. ...
    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 full-size slide

  37. }
    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 full-size slide

  38. 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 full-size slide

  39. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  50. 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 full-size slide

  51. αϯϓϧίʔυ

    View full-size slide

  52. 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 full-size slide

  53. @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 full-size slide

  54. @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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  61. @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 full-size slide

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

    View full-size slide

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

    View full-size slide

  64. @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 full-size slide

  65. @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 full-size slide

  66. @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 full-size slide

  67. @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()
    }

    View full-size slide

  68. @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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  72. 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 full-size slide

  73. 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 full-size slide

  74. 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 full-size slide

  75. @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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide