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

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

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

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

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

1763273018a6f71ffe4162dd57b60a56?s=128

Seungmin - maryang

October 20, 2019
Tweet

Transcript

  1. 2.
  2. 5.
  3. 7.

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

    Architecture ٜ݅ӝ 4. Testable Architecture ৮ࢿೞӝ 5. Testable Culture 6. Ѿۿ
  4. 10.

    పझ౟ۆ? যڃ ز੘ਸ ഐ୹೮ਸ ٸ, 1. ч੉ ਗೞח ഋకо غח૑

    Ѩૐ 2. ਗೞח ز੘ਸ ো҅೧ࢲ ࣻ೯ೞחо Ѩૐ
  5. 17.
  6. 18.
  7. 24.

    class GithubRepoPresenter( view: GithubRepoView, repository: GithubRepository ) { fun issues(repo:

    GithubRepo) = repository.issues(owner, repo.name) .subscribe( { view.showIssues(it) }, { it.printStackTrace() } ) }
  8. 25.

    class GithubRepoPresenter( view: GithubRepoView, repository: GithubRepository ) { fun issues(repo:

    GithubRepo) = repository.issues(owner, repo.name) .subscribe( { view.showIssues(it) }, { it.printStackTrace() } ) } ੄ઓ௿ېझ
  9. 26.

    class GithubRepoPresenter( view: GithubRepoView, repository: GithubRepository ) { fun issues(repo:

    GithubRepo) = repository.issues(owner, repo.name) .subscribe( { view.showIssues(it) }, { it.printStackTrace() } ) } যڌѱز੘  ޖ঺ਸ߈ജ
  10. 27.

    class GithubReposPresenterTest { private lateinit var presenter: GithubReposPresenter @Mock lateinit

    var view: GithubReposView @Mock lateinit var githubRepository: GithubRepository @Before fun setUp() { presenter = GithubReposPresenter( view, githubRepository ) } }
  11. 28.

    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ഝਊ
  12. 29.

    // ߈ജ чਸ ૑੿ val repos: List<GithubRepo> = emptyList() //

    Mock Class੄ ز੘ਸ ੿੄ Mockito.`when`( githubRepository.searchGithubRepos(anyString()) ).thenReturn(Single.just(repos))
  13. 30.

    // ߈ജ чਸ ૑੿ val repos: List<GithubRepo> = emptyList() //

    Mock Class੄ ز੘ਸ ੿੄ Mockito.`when`( githubRepository.searchGithubRepos(anyString()) ).thenReturn(Single.just(repos)) 4UVC .PDLJOH
  14. 33.

    class GithubReposPresenter( private val connectivityManager: ConnectivityManager, ) : BasePresenter<GithubReposView>(view) {

    fun onCreate() { if (connectivityManager.isConnected().not()) view.showNoInternetWarning() } }
  15. 34.

    class GithubReposPresenter( private val connectivityManager: ConnectivityManager, ) : BasePresenter<GithubReposView>(view) {

    fun onCreate() { if (connectivityManager.isConnected().not()) view.showNoInternetWarning() } } $POUFYU੄ઓೞח ੋఠ֔୓௼ӝמࢎਊ
  16. 35.

    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() }
  17. 36.

    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 উ٘۽੉٘੄ઓݫࣗ٘ࢎਊ
  18. 38.

    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() } }
  19. 39.

    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
  20. 41.

    1. ੄ઓ Class੄ ز੘ - Mocking, Stub 2. উ٘۽੉٘ ੄ઓ

    ز੘ - Wrapping, Mocking పझ౟ܳ য۵ѱ ೞח Ѫ
  21. 42.

    1. ੄ઓ Class੄ ز੘ - Mocking, Stub 2. উ٘۽੉٘ ੄ઓ

    ز੘ - Wrapping, Mocking పझ౟ܳ য۵ѱ ೞח Ѫ ਤޙઁоऔѱ೧䟽غযঠపझ౟ܳೡࣻ੓׮
  22. 43.

    1. ੄ઓ Class੄ ز੘ - Mocking, Stub 2. উ٘۽੉٘ ੄ઓ

    ز੘ - Wrapping, Mocking పझ౟ܳ য۵ѱ ೞח Ѫ ਤޙઁоऔѱ೧䟽غযঠపझ౟ܳೡࣻ੓׮ 5FTUBCMF"SDIJUFDUVSF
  23. 46.

    ௏٘੄ ৉ೡਸ ܻ࠙ೞৈ ৈ۞ ੉੼ਸ ঳ח׮ 1. оةࢿ (֫਷ ਽૘ب)

    2. ߸҃ ਬোೣ (ծ਷ Ѿ೤ب) উ٘۽੉٘ীࢲח MVP, MVVMਸ ઱۽ ࢎਊ Architectureۆ?
  24. 47.

    ௏٘੄ ৉ೡਸ ܻ࠙ೞৈ ৈ۞ ੉੼ਸ ঳ח׮ 1. оةࢿ (֫਷ ਽૘ب)

    2. ߸҃ ਬোೣ (ծ਷ Ѿ೤ب) উ٘۽੉٘ীࢲח MVP, MVVMਸ ઱۽ ࢎਊ Architectureۆ? Ӓրࢎਊೞݶ/PU5FTUBCMFഛܫ੉֫਺ ݻਗ஗ਸӝ߈ਵ۽ѐ䣜ೞݶ5FTUBCMF"SDIJUFDUVSF
  25. 49.

    class GithubRepoBadPresenter( view: GithubRepoView ) : BasePresenter<GithubRepoView>(view) { private val

    repository = GithubRepository() fun save(repo: GithubRepo) = repository.save(repo) .subscribe() } Bad Case 1 - ੄ઓࢿ ࢤࢿ
  26. 50.

    class GithubRepoBadPresenter( view: GithubRepoView ) : BasePresenter<GithubRepoView>(view) { private val

    repository = GithubRepository() fun save(repo: GithubRepo) = repository.save(repo) .subscribe() } ੄ઓࢿ੗୓ࢤࢿ Bad Case 1 - ੄ઓࢿ ࢤࢿ
  27. 51.

    class GithubRepoBadPresenter( view: GithubRepoView ) : BasePresenter<GithubRepoView>(view) { private val

    repository = GithubRepository() fun save(repo: GithubRepo) = repository.save(repo) .subscribe() } .PDL3FQPTJUPSZ઱ੑࠛо పझ౟ࠛо Bad Case 1 - ੄ઓࢿ ࢤࢿ
  28. 52.

    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
  29. 53.

    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 ੄ઓࢿ઱ੑ߉਺
  30. 54.

    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ഝਊ పझ౟оמ
  31. 55.

    class GithubRepoActivity { override val presenter: GithubRepoPresenter by lazy {

    GithubRepoPresenter(this, GithubRepository()) } } ࠗ۾
  32. 56.

    class GithubRepoActivity { override val presenter: GithubRepoPresenter by lazy {

    GithubRepoPresenter(this, GithubRepository()) } } ࠗ۾ 6*ী䞵%BUBଵઑ
  33. 57.

    class GithubRepoActivity { override val presenter: GithubRepoPresenter by lazy {

    GithubRepoPresenter(this, GithubRepository()) } } ࠗ۾ 6*ী䞵%BUBଵઑ  %*
  34. 58.

    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
  35. 59.

    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
  36. 61.

    class IssueCreatePresenter( view: IssueCreateView, private val dataObserver: DataObserver, private val

    repository: GithubRepository ) Good Case 4UBUJD$MBTTܳੌ߈$MBTT۽߸҃ ੄ઓࢿ઱ੑ߉਺
  37. 64.

    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
  38. 65.

    interface GithubReposView { fun showNoInternetWarning() fun showLoading() fun hideLoading() fun

    showRepos(repos: List<GithubRepo>) } fun onCreate() { if (networkHelper.isConnected().not()) view.showNoInternetWarning() } UI Method
  39. 66.

    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
  40. 67.

    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 ాन
  41. 68.

    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
  42. 69.

    ਗ஗ 2 - উ٘۽੉٘ ੄ઓ ز੘ਸ Wrapping ೠ׮ 4%,.FUIPE6*%# "1*ాन

    4%,.FUIPE8SBQQFS 7JFX*OUFSGBDF 3FQPTJUPSZ .71੄ӝࠄ
  43. 71.

    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
  44. 72.

    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
  45. 74.

    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प೯݅
  46. 76.

    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 - ചݶ ղ ۽૒
  47. 77.

    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 - ചݶ ղ ۽૒ ഐ୹ৈࠗపझ౟ࠛо
  48. 78.

    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
  49. 79.

    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प೯݅
  50. 80.

    @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۽૒పझ౟оמ
  51. 86.

    @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ദٙ
  52. 93.

    1. ੄ઓ Class੄ ز੘ - Mocking, Stub 2. উ٘۽੉٘ ੄ઓ

    ز੘ - Wrapping, Mocking పझ౟ܳ য۵ѱ ೞח Ѫ
  53. 94.

    1. ੄ઓ Class੄ ز੘ - Mocking, Stub 2. উ٘۽੉٘ ੄ઓ

    ز੘ - Wrapping, Mocking పझ౟ܳ য۵ѱ ೞח Ѫ 2"۽䟰ૐೞחѪ੉ࡅܰ׮ Ҋוԑ૓׮  పझ౟ܳ୶оೡदр੉হ׮
  54. 95.

    1. ੄ઓ Class੄ ز੘ - Mocking, Stub 2. উ٘۽੉٘ ੄ઓ

    ز੘ - Wrapping, Mocking పझ౟ܳ য۵ѱ ೞח Ѫ పझ౟੄ബਊࢿਸݽܰѷ׮ ੌ੿੉ցޖ߄ࡅزӝо࢓૑ঋח׮
  55. 100.

    1. ز੘ ഛੋ 2. ߸҃ ਬোࢿ ഛࠁ 3. ௏ܻ٘࠭ܳ ذח׮

    పझ౟੄ ബਊࢿ పझ౟ח੄بܳݺदച పझ౟ܳ౵ঈೞҊ௏٘ܳࠁݶܻ࠭оࣻ䤰ೞ׮
  56. 104.

    1. ܻನ౟۽ അपਸ ӵײ੗ 2. ੊ࣼ೧૑ӝө૑ ъઁࢿਸ ف੗ 3. ए਍

    Ѫࠗఠ ରӔରӔ పझ౟ زӝࠗৈ 1SFTFOUFS7.5FTU 6OJU5FTU 1SFTFOUFS6OJUపझ౟о ࣘبоࡅܰҊ ࣻݺ੉ӡ׮
  57. 107.
  58. 108.

    ਗ஗ 1. ੄ઓࢿ ё୓ח ݽف ઱ੑ߉ח׮ 2. উ٘۽੉٘ ੄ઓ ز੘ਸ

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