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

테스트 관점으로 아키텍쳐 완성하기 (Testable Architecture)

테스트 관점으로 아키텍쳐 완성하기 (Testable Architecture)

- GDG DevFest Seoul 2019 (10.20)
- Line TechTalk 사내발표 (11.18)
위 행사들에서 발표한 자료입니다.

안드로이드 개발 3년차 김데페는 MVP를 다룹니다. RxJava로 API를 호출하고 DI도 알아요. 하지만 최근 공부를 시작한 테스트가 잘 짜여지지 않습니다. 그나마 동작하는 몇 테스트도 효용이 있는지 모르겠습니다.

테스트와 아키텍쳐는 함께 움직이는 것을 아시나요? 테스트가 불편하거나 효용성이 적은 것은 아키텍쳐가 불안정하기 때문입니다. 이 발표는 MVP 아키텍쳐를 테스트 관점으로 완성시켜봅니다.

Seungmin 마량

October 20, 2019
Tweet

More Decks by Seungmin 마량

Other Decks in Programming

Transcript

  1. ݾର 1. పझ౟ۆ? 2. పझ౟ܳ য۵ѱ ೞח Ѫ 3. Testable

    Architecture ٜ݅ӝ 4. Testable Architecture ৮ࢿೞӝ 5. Testable Culture 6. Ѿۿ
  2. పझ౟ۆ? যڃ ز੘ਸ ഐ୹೮ਸ ٸ, 1. ч੉ ਗೞח ഋకо غח૑

    Ѩૐ 2. ਗೞח ز੘ਸ ো҅೧ࢲ ࣻ೯ೞחо Ѩૐ
  3. class GithubRepoPresenter( view: GithubRepoView, repository: GithubRepository ) { fun issues(repo:

    GithubRepo) = repository.issues(owner, repo.name) .subscribe( { view.showIssues(it) }, { it.printStackTrace() } ) }
  4. class GithubRepoPresenter( view: GithubRepoView, repository: GithubRepository ) { fun issues(repo:

    GithubRepo) = repository.issues(owner, repo.name) .subscribe( { view.showIssues(it) }, { it.printStackTrace() } ) } ੄ઓ௿ېझ
  5. class GithubRepoPresenter( view: GithubRepoView, repository: GithubRepository ) { fun issues(repo:

    GithubRepo) = repository.issues(owner, repo.name) .subscribe( { view.showIssues(it) }, { it.printStackTrace() } ) } যڌѱز੘  ޖ঺ਸ߈ജ
  6. class GithubReposPresenterTest { private lateinit var presenter: GithubReposPresenter @Mock lateinit

    var view: GithubReposView @Mock lateinit var githubRepository: GithubRepository @Before fun setUp() { presenter = GithubReposPresenter( view, githubRepository ) } }
  7. class GithubReposPresenterTest { private lateinit var presenter: GithubReposPresenter @Mock lateinit

    var view: GithubReposView @Mock lateinit var githubRepository: GithubRepository @Before fun setUp() { presenter = GithubReposPresenter( view, githubRepository ) } } .PDLഝਊ
  8. // ߈ജ чਸ ૑੿ val repos: List<GithubRepo> = emptyList() //

    Mock Class੄ ز੘ਸ ੿੄ Mockito.`when`( githubRepository.searchGithubRepos(anyString()) ).thenReturn(Single.just(repos))
  9. // ߈ജ чਸ ૑੿ val repos: List<GithubRepo> = emptyList() //

    Mock Class੄ ز੘ਸ ੿੄ Mockito.`when`( githubRepository.searchGithubRepos(anyString()) ).thenReturn(Single.just(repos)) 4UVC .PDLJOH
  10. class GithubReposPresenter( private val connectivityManager: ConnectivityManager, ) : BasePresenter<GithubReposView>(view) {

    fun onCreate() { if (connectivityManager.isConnected().not()) view.showNoInternetWarning() } }
  11. class GithubReposPresenter( private val connectivityManager: ConnectivityManager, ) : BasePresenter<GithubReposView>(view) {

    fun onCreate() { if (connectivityManager.isConnected().not()) view.showNoInternetWarning() } } $POUFYU੄ઓೞח ੋఠ֔୓௼ӝמࢎਊ
  12. class GithubReposPresenter( private val connectivityManager: ConnectivityManager, ) : BasePresenter<GithubReposView>(view) {

    fun onCreate() { if (connectivityManager.isConnected().not()) view.showNoInternetWarning() } } @Test fun internetCheckTest() { Mockito.`when`(connectivityManagerMock.isConnected()).thenReturn(false) presenter.onCreate() Mockito.verify(view).showNoInternetWarning() }
  13. class GithubReposPresenter( private val connectivityManager: ConnectivityManager, ) : BasePresenter<GithubReposView>(view) {

    fun onCreate() { if (connectivityManager.isConnected().not()) view.showNoInternetWarning() } } @Test fun internetCheckTest() { Mockito.`when`(connectivityManagerMock.isConnected()).thenReturn(false) presenter.onCreate() Mockito.verify(view).showNoInternetWarning() } &SSPS উ٘۽੉٘੄ઓݫࣗ٘ࢎਊ
  14. class NetworkHelper(context: Context) { private val connectivityManager: ConnectivityManager fun isConnected():

    Boolean = connectivityManager.isConnected() } class GithubReposPresenter( private val networkHelper: NetworkHelper, ) : BasePresenter<GithubReposView>(view) { fun onCreate() { if (networkHelper.isConnected().not()) view.showNoInternetWarning() } }
  15. class NetworkHelper(context: Context) { private val connectivityManager: ConnectivityManager fun isConnected():

    Boolean = connectivityManager.isConnected() } class GithubReposPresenter( private val networkHelper: NetworkHelper, ) : BasePresenter<GithubReposView>(view) { fun onCreate() { if (networkHelper.isConnected().not()) view.showNoInternetWarning() } } $POUFYUӝמ 8SBQQJOH
  16. 1. ੄ઓ Class੄ ز੘ - Mocking, Stub 2. উ٘۽੉٘ ੄ઓ

    ز੘ - Wrapping, Mocking పझ౟ܳ য۵ѱ ೞח Ѫ
  17. 1. ੄ઓ Class੄ ز੘ - Mocking, Stub 2. উ٘۽੉٘ ੄ઓ

    ز੘ - Wrapping, Mocking పझ౟ܳ য۵ѱ ೞח Ѫ ਤޙઁоऔѱ೧䟽غযঠపझ౟ܳೡࣻ੓׮
  18. 1. ੄ઓ Class੄ ز੘ - Mocking, Stub 2. উ٘۽੉٘ ੄ઓ

    ز੘ - Wrapping, Mocking పझ౟ܳ য۵ѱ ೞח Ѫ ਤޙઁоऔѱ೧䟽غযঠపझ౟ܳೡࣻ੓׮ 5FTUBCMF"SDIJUFDUVSF
  19. ௏٘੄ ৉ೡਸ ܻ࠙ೞৈ ৈ۞ ੉੼ਸ ঳ח׮ 1. оةࢿ (֫਷ ਽૘ب)

    2. ߸҃ ਬোೣ (ծ਷ Ѿ೤ب) উ٘۽੉٘ীࢲח MVP, MVVMਸ ઱۽ ࢎਊ Architectureۆ?
  20. ௏٘੄ ৉ೡਸ ܻ࠙ೞৈ ৈ۞ ੉੼ਸ ঳ח׮ 1. оةࢿ (֫਷ ਽૘ب)

    2. ߸҃ ਬোೣ (ծ਷ Ѿ೤ب) উ٘۽੉٘ীࢲח MVP, MVVMਸ ઱۽ ࢎਊ Architectureۆ? Ӓրࢎਊೞݶ/PU5FTUBCMFഛܫ੉֫਺ ݻਗ஗ਸӝ߈ਵ۽ѐ䣜ೞݶ5FTUBCMF"SDIJUFDUVSF
  21. class GithubRepoBadPresenter( view: GithubRepoView ) : BasePresenter<GithubRepoView>(view) { private val

    repository = GithubRepository() fun save(repo: GithubRepo) = repository.save(repo) .subscribe() } Bad Case 1 - ੄ઓࢿ ࢤࢿ
  22. class GithubRepoBadPresenter( view: GithubRepoView ) : BasePresenter<GithubRepoView>(view) { private val

    repository = GithubRepository() fun save(repo: GithubRepo) = repository.save(repo) .subscribe() } ੄ઓࢿ੗୓ࢤࢿ Bad Case 1 - ੄ઓࢿ ࢤࢿ
  23. class GithubRepoBadPresenter( view: GithubRepoView ) : BasePresenter<GithubRepoView>(view) { private val

    repository = GithubRepository() fun save(repo: GithubRepo) = repository.save(repo) .subscribe() } .PDL3FQPTJUPSZ઱ੑࠛо పझ౟ࠛо Bad Case 1 - ੄ઓࢿ ࢤࢿ
  24. class GithubRepoGoodPresenter( view: GithubRepoView, private val repository: GithubRepository ) :

    BasePresenter<GithubRepoView>(view) class GithubRepoPresenterTest : BasePresenterTest() { @Mock lateinit var githubRepository: GithubRepository override fun setUp() { presenter = GithubRepoPresenter( view, githubRepository ) } Good Case
  25. class GithubRepoGoodPresenter( view: GithubRepoView, private val repository: GithubRepository ) :

    BasePresenter<GithubRepoView>(view) class GithubRepoPresenterTest : BasePresenterTest() { @Mock lateinit var githubRepository: GithubRepository override fun setUp() { presenter = GithubRepoPresenter( view, githubRepository ) } Good Case ੄ઓࢿ઱ੑ߉਺
  26. class GithubRepoGoodPresenter( view: GithubRepoView, private val repository: GithubRepository ) :

    BasePresenter<GithubRepoView>(view) class GithubRepoPresenterTest : BasePresenterTest() { @Mock lateinit var githubRepository: GithubRepository override fun setUp() { presenter = GithubRepoPresenter( view, githubRepository ) } Good Case .PDLഝਊ పझ౟оמ
  27. class GithubRepoActivity { override val presenter: GithubRepoPresenter by lazy {

    GithubRepoPresenter(this, GithubRepository()) } } ࠗ۾
  28. class GithubRepoActivity { override val presenter: GithubRepoPresenter by lazy {

    GithubRepoPresenter(this, GithubRepository()) } } ࠗ۾ 6*ী䞵%BUBଵઑ
  29. class GithubRepoActivity { override val presenter: GithubRepoPresenter by lazy {

    GithubRepoPresenter(this, GithubRepository()) } } ࠗ۾ 6*ী䞵%BUBଵઑ  %*
  30. class IssueCreatePresenter { fun createIssue(repo: GithubRepo, title: String, body: String)

    = repository.createIssue(owner, name, title, body) .subscribe({ DataObserver.post(it) view.onIssueCreated() }, { view.onIssueCreateFail() }) } Bad Case 2 - Static Class
  31. class IssueCreatePresenter { fun createIssue(repo: GithubRepo, title: String, body: String)

    = repository.createIssue(owner, name, title, body) .subscribe({ DataObserver.post(it) view.onIssueCreated() }, { view.onIssueCreateFail() }) } 4UBUJD$MBTT ઱ੑࠛоపझ౟ࠛо Bad Case 2 - Static Class
  32. class IssueCreatePresenter( view: IssueCreateView, private val dataObserver: DataObserver, private val

    repository: GithubRepository ) Good Case 4UBUJD$MBTTܳੌ߈$MBTT۽߸҃ ੄ઓࢿ઱ੑ߉਺
  33. class NetworkHelper(context: Context) { private val connectivityManager: ConnectivityManager fun isConnected():

    Boolean = connectivityManager.isConnected() } class GithubReposPresenter( private val networkHelper: NetworkHelper, ) : BasePresenter<GithubReposView>(view) { fun onCreate() { if (networkHelper.isConnected().not()) view.showNoInternetWarning() } } $POUFYUӝמ 8SBQQJOH SDK Method
  34. interface GithubReposView { fun showNoInternetWarning() fun showLoading() fun hideLoading() fun

    showRepos(repos: List<GithubRepo>) } fun onCreate() { if (networkHelper.isConnected().not()) view.showNoInternetWarning() } UI Method
  35. interface GithubReposView { fun showNoInternetWarning() fun showLoading() fun hideLoading() fun

    showRepos(repos: List<GithubRepo>) } fun onCreate() { if (networkHelper.isConnected().not()) view.showNoInternetWarning() } 7JFX*OUFSGBDF۽ 8SBQQJOH UI Method
  36. interface GithubRepository { fun save(repo: GithubRepo): Completable fun star(owner: String,

    repo: String): Completable fun unstar(owner: String, repo: String): Completable } class IssueCreatePresenter( private val repository: GithubRepository ) { fun createIssue(repo: GithubRepo, title: String, body: String) = repository.createIssue(owner, repo.name, title, body) .subscribe{ view.onIssueCreated() } } DB / API ాन
  37. interface GithubRepository { fun save(repo: GithubRepo): Completable fun star(owner: String,

    repo: String): Completable fun unstar(owner: String, repo: String): Completable } class IssueCreatePresenter( private val repository: GithubRepository ) { fun createIssue(repo: GithubRepo, title: String, body: String) = repository.createIssue(owner, repo.name, title, body) .subscribe{ view.onIssueCreated() } } DB / API ాन 3FQPTJUPSZ*OUFSGBDF۽ 8SBQQJOH
  38. ਗ஗ 2 - উ٘۽੉٘ ੄ઓ ز੘ਸ Wrapping ೠ׮ 4%,.FUIPE6*%# "1*ాन

    4%,.FUIPE8SBQQFS 7JFX*OUFSGBDF 3FQPTJUPSZ .71੄ӝࠄ
  39. override fun onCreate(savedInstanceState: Bundle?) { showRepo(intent.getParcelableExtra<GithubRepo>(KEY_REPO)) } private fun showRepo(repo:

    GithubRepo) { ownerName.text = repo.owner.userName starCount.text = repo.stargazersCount.toString() watcherCount.text = repo.watchersCount.toString() forksCount.text = repo.forksCount.toString() showStar(repo.star) presenter.issues(repo) } Bad Case 1 - ചݶ ղ private Method
  40. override fun onCreate(savedInstanceState: Bundle?) { // showRepo(intent.getParcelableExtra<GithubRepo>(KEY_REPO)) } private fun

    showRepo(repo: GithubRepo) { ownerName.text = repo.owner.userName starCount.text = repo.stargazersCount.toString() watcherCount.text = repo.watchersCount.toString() forksCount.text = repo.forksCount.toString() showStar(repo.star) presenter.issues(repo) } ഐ୹ৈࠗపझ౟ࠛо Bad Case 1 - ചݶ ղ private Method
  41. override fun onCreate(savedInstanceState: Bundle?) { presenter.onCreate(it) } fun onCreate(repo: GithubRepo)

    { this.repo = repo view.showRepo(repo) issues(repo) } Good Case 1SFTFOUFSী䞵۽૒ࣻ೯ 7JFXח1SFTFOUFSप೯݅
  42. star.onClick { val originalStar = repo.star showStar(!originalStar) showStarCount(repo.stargazersCount.let { if

    (originalStar) it - 1 else it + 1 }) presenter.onClickStar() } fun onClickStar() { (if (originalStar) repository.unstar(owner, repo.name) else repository.star(owner, repo.name)) ... } Bad Case 2 - ചݶ ղ ۽૒
  43. star.onClick { val originalStar = repo.star showStar(!originalStar) showStarCount(repo.stargazersCount.let { if

    (originalStar) it - 1 else it + 1 }) presenter.onClickStar() } fun onClickStar() { (if (originalStar) repository.unstar(owner, repo.name) else repository.star(owner, repo.name)) ... } Bad Case 2 - ചݶ ղ ۽૒ ഐ୹ৈࠗపझ౟ࠛо
  44. star.onClick { presenter.onClickStar() } fun onClickStar() { val originalStar =

    repo.star view.showStar(!originalStar) view.showStarCount(repo.stargazersCount.let { if (originalStar) it - 1 else it + 1 }) (if (originalStar) repository.unstar(owner, repo.name) else repository.star(owner, repo.name)) ... } Good Case
  45. star.onClick { presenter.onClickStar() } fun onClickStar() { val originalStar =

    repo.star view.showStar(!originalStar) view.showStarCount(repo.stargazersCount.let { if (originalStar) it - 1 else it + 1 }) (if (originalStar) repository.unstar(owner, repo.name) else repository.star(owner, repo.name)) ... } Good Case 1SFTFOUFSী䞵۽૒ࣻ೯ 7JFXח1SFTFOUFSप೯݅
  46. @Test fun clickStarTest() { repo.star = false val originalStarCount =

    repo.stargazersCount presenter.onClickStar() Mockito.verify(view).showStar(true) Mockito.verify(view).showStarCount(originalStarCount + 1) } Good Case 1SFTFOUFS۽૒పझ౟оמ
  47. @RunWith(AndroidJUnit4::class) class PreferenceTest { @Test fun preferenceTest() { val context

    = InstrumentationRegistry.getInstrumentation().context val pref = context.getSharedPreferences("preference") val userName = pref.getString("pref_user_name", "userName") assertEquals("userName", userName) } } $POUFYUദٙ
  48. 1. ੄ઓ Class੄ ز੘ - Mocking, Stub 2. উ٘۽੉٘ ੄ઓ

    ز੘ - Wrapping, Mocking పझ౟ܳ য۵ѱ ೞח Ѫ
  49. 1. ੄ઓ Class੄ ز੘ - Mocking, Stub 2. উ٘۽੉٘ ੄ઓ

    ز੘ - Wrapping, Mocking పझ౟ܳ য۵ѱ ೞח Ѫ 2"۽䟰ૐೞחѪ੉ࡅܰ׮ Ҋוԑ૓׮  పझ౟ܳ୶оೡदр੉হ׮
  50. 1. ੄ઓ Class੄ ز੘ - Mocking, Stub 2. উ٘۽੉٘ ੄ઓ

    ز੘ - Wrapping, Mocking పझ౟ܳ য۵ѱ ೞח Ѫ పझ౟੄ബਊࢿਸݽܰѷ׮ ੌ੿੉ցޖ߄ࡅزӝо࢓૑ঋח׮
  51. 1. ز੘ ഛੋ 2. ߸҃ ਬোࢿ ഛࠁ 3. ௏ܻ٘࠭ܳ ذח׮

    పझ౟੄ ബਊࢿ పझ౟ח੄بܳݺदച పझ౟ܳ౵ঈೞҊ௏٘ܳࠁݶܻ࠭оࣻ䤰ೞ׮
  52. 1. ܻನ౟۽ അपਸ ӵײ੗ 2. ੊ࣼ೧૑ӝө૑ ъઁࢿਸ ف੗ 3. ए਍

    Ѫࠗఠ ରӔରӔ పझ౟ زӝࠗৈ 1SFTFOUFS7.5FTU 6OJU5FTU 1SFTFOUFS6OJUపझ౟о ࣘبоࡅܰҊ ࣻݺ੉ӡ׮
  53. ਗ஗ 1. ੄ઓࢿ ё୓ח ݽف ઱ੑ߉ח׮ 2. উ٘۽੉٘ ੄ઓ ز੘ਸ

    Wrapping ೠ׮ 3. ݽٚ ز੘਷ Presenterܳ ాೠ׮