Slide 1

Slide 1 text

How to improve your MVP architecture and tests DroidKaigi 2018 @kirimin

Slide 2

Slide 2 text

About me

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

MVP

Slide 10

Slide 10 text

Model View Presenter

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

Ͳ͏ͯ͜͠Μͳ͜ͱʹ…

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

Ξϯνύλʔϯͷ վળҊ

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

αϯϓϧίʔυ

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

·ͣ͸Ξϯνύλʔϯ

Slide 33

Slide 33 text

View(Activity)

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

Presenter

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

վળޙ

Slide 40

Slide 40 text

View(Activity)

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

Presenter

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

MockitoΛ࢖͏ • JavaͷϝδϟʔͳϞοΫϥΠϒϥϦ • Ϋϥε΍ΠϯλʔϑΣΠεͷϞοΫΠϯελϯεΛੜ ੒͠ɺϝιουʹ೚ҙͷ໭Γ஋Λࢦఆͨ͠Γɺϝιο υ͕ݺ͹Εͨճ਺Λ֬ೝͨ͠Γग़དྷΔ // TopViewͷϞοΫΠϯελϯεΛੜ੒ viewMock = mock(TopView::class.java) // presenterͷonCreate()ϝιουΛςετ಺ͰݺͿ presenter = TopPresenter(viewMock, useCaseMock) presenter.onCreate() // ݁ՌviewMockͷinitView()ϝιου͕ݺ͹Ε͍ͯΔࣄΛ֬ೝ verify(viewMock, Mockito.times(1)).initView()

Slide 58

Slide 58 text

αϯϓϧίʔυ

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

1೥ޙ...

Slide 65

Slide 65 text

No content

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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ϝιου

Slide 82

Slide 82 text

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) } υϝΠϯϞσϧ಺ͰॲཧΛߦ͍ ஋Λอ࣋͢ΔΑ͏ʹมߋ

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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