Share
Embed
테스트 관점으로 아키텍쳐 완성하기 (Testable Architecture)
- GDG DevFest Seoul 2019 (10.20)
- Line TechTalk 사내발표 (11.18)
위 행사들에서 발표한 자료입니다.
안드로이드 개발 3년차 김데페는 MVP를 다룹니다. RxJava로 API를 호출하고 DI도 알아요. 하지만 최근 공부를 시작한 테스트가 잘 짜여지지 않습니다. 그나마 동작하는 몇 테스트도 효용이 있는지 모르겠습니다.
테스트와 아키텍쳐는 함께 움직이는 것을 아시나요? 테스트가 불편하거나 효용성이 적은 것은 아키텍쳐가 불안정하기 때문입니다. 이 발표는 MVP 아키텍쳐를 테스트 관점으로 완성시켜봅니다.
Seungmin 마량
October 20, 2019
Transcript
115 slides
-
- MVPܳ ܛפ - RxJava۽ APIܳ ഐפ. - DIܳ ঌইਃ
3֙ର ѐߊ ӣؘಕח… -
ݾର 1. పझۆ? 2. పझܳ য۵ѱ ೞח Ѫ 3. Testable
Architecture ٜ݅ӝ 4. Testable Architecture ৮ࢿೞӝ 5. Testable Culture 6. Ѿۿ -
పझۆ? যڃ زਸ ഐ೮ਸ ٸ, 1. ч ਗೞח ഋకо غח
Ѩૐ 2. ਗೞח زਸ ো҅೧ࢲ ࣻ೯ೞחо Ѩૐ -
@Test fun loginTest() { loginHelper.login() // isLogin ч Trueо غחо?
assertTrue(loginHelper.isLogin) } -
@Test fun loginTest() { loginHelper.login() // isLogin ч Trueо غחо?
assertTrue(loginHelper.isLogin) } -
@Test fun loginTest() { loginHelper.login() // isLogin ч Trueо غחо?
assertTrue(loginHelper.isLogin) } -
పझ ਗܻ ѨૐೞҊ ೞח ݫࣗ٘ܳ ࣻ೯दௌਸ ٸ, 1. ਗೞח ч
assert()о true 2. ਗೞח ز verify()о true -
పझ ਗܻ ѨૐೞҊ ೞח ݫࣗ٘ܳ ࣻ೯दௌਸ ٸ, 1. ਗೞח ч
assert()о true 2. ਗೞח ز verify()о true -
class GithubReposPresenterTest { private lateinit var presenter: GithubReposPresenter @Before fun
setUp() { presenter = GithubReposPresenter() } } -
class GithubReposPresenterTest { private lateinit var presenter: GithubReposPresenter @Before fun
setUp() { presenter = GithubReposPresenter() } } -
class GithubRepoPresenter( view: GithubRepoView, repository: GithubRepository ) { fun issues(repo:
GithubRepo) = repository.issues(owner, repo.name) .subscribe( { view.showIssues(it) }, { it.printStackTrace() } ) } -
class GithubRepoPresenter( view: GithubRepoView, repository: GithubRepository ) { fun issues(repo:
GithubRepo) = repository.issues(owner, repo.name) .subscribe( { view.showIssues(it) }, { it.printStackTrace() } ) } ઓېझ -
class GithubRepoPresenter( view: GithubRepoView, repository: GithubRepository ) { fun issues(repo:
GithubRepo) = repository.issues(owner, repo.name) .subscribe( { view.showIssues(it) }, { it.printStackTrace() } ) } যڌѱز ޖਸ߈ജ -
class GithubReposPresenterTest { private lateinit var presenter: GithubReposPresenter @Mock lateinit
var view: GithubReposView @Mock lateinit var githubRepository: GithubRepository @Before fun setUp() { presenter = GithubReposPresenter( view, githubRepository ) } } -
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ഝਊ -
// ߈ജ чਸ val repos: List<GithubRepo> = emptyList() //
Mock Class زਸ Mockito.`when`( githubRepository.searchGithubRepos(anyString()) ).thenReturn(Single.just(repos)) -
// ߈ജ чਸ val repos: List<GithubRepo> = emptyList() //
Mock Class زਸ Mockito.`when`( githubRepository.searchGithubRepos(anyString()) ).thenReturn(Single.just(repos)) 4UVC .PDLJOH -
1. ઓ Class ز - Mocking, Stub 2. উ٘۽٘ ઓ
ز పझܳ য۵ѱ ೞח Ѫ -
class GithubReposPresenter( private val connectivityManager: ConnectivityManager, ) : BasePresenter<GithubReposView>(view) {
fun onCreate() { if (connectivityManager.isConnected().not()) view.showNoInternetWarning() } } -
class GithubReposPresenter( private val connectivityManager: ConnectivityManager, ) : BasePresenter<GithubReposView>(view) {
fun onCreate() { if (connectivityManager.isConnected().not()) view.showNoInternetWarning() } } $POUFYUઓೞח ੋఠ֔ӝמࢎਊ -
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() } -
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 উ٘۽٘ઓݫࣗ٘ࢎਊ -
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() } } -
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 -
1. ઓ Class ز - Mocking, Stub 2. উ٘۽٘ ઓ
ز - Wrapping, Mocking పझܳ য۵ѱ ೞח Ѫ -
1. ઓ Class ز - Mocking, Stub 2. উ٘۽٘ ઓ
ز - Wrapping, Mocking పझܳ য۵ѱ ೞח Ѫ ਤޙઁоऔѱ೧䟽غযঠపझܳೡࣻ -
1. ઓ Class ز - Mocking, Stub 2. উ٘۽٘ ઓ
ز - Wrapping, Mocking పझܳ য۵ѱ ೞח Ѫ ਤޙઁоऔѱ೧䟽غযঠపझܳೡࣻ 5FTUBCMF"SDIJUFDUVSF -
٘ ೡਸ ܻ࠙ೞৈ ৈ۞ ਸ ח 1. оةࢿ (֫ ب)
2. ߸҃ ਬোೣ (ծ Ѿب) উ٘۽٘ীࢲח MVP, MVVMਸ ۽ ࢎਊ Architectureۆ? -
٘ ೡਸ ܻ࠙ೞৈ ৈ۞ ਸ ח 1. оةࢿ (֫ ب)
2. ߸҃ ਬোೣ (ծ Ѿب) উ٘۽٘ীࢲח MVP, MVVMਸ ۽ ࢎਊ Architectureۆ? Ӓրࢎਊೞݶ/PU5FTUBCMFഛܫ֫ ݻਗਸӝ߈ਵ۽ѐ䣜ೞݶ5FTUBCMF"SDIJUFDUVSF -
class GithubRepoBadPresenter( view: GithubRepoView ) : BasePresenter<GithubRepoView>(view) { private val
repository = GithubRepository() fun save(repo: GithubRepo) = repository.save(repo) .subscribe() } Bad Case 1 - ઓࢿ ࢤࢿ -
class GithubRepoBadPresenter( view: GithubRepoView ) : BasePresenter<GithubRepoView>(view) { private val
repository = GithubRepository() fun save(repo: GithubRepo) = repository.save(repo) .subscribe() } ઓࢿࢤࢿ Bad Case 1 - ઓࢿ ࢤࢿ -
class GithubRepoBadPresenter( view: GithubRepoView ) : BasePresenter<GithubRepoView>(view) { private val
repository = GithubRepository() fun save(repo: GithubRepo) = repository.save(repo) .subscribe() } .PDL3FQPTJUPSZੑࠛо పझࠛо Bad Case 1 - ઓࢿ ࢤࢿ -
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 -
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 ઓࢿੑ߉ -
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ഝਊ పझоמ -
class GithubRepoActivity { override val presenter: GithubRepoPresenter by lazy {
GithubRepoPresenter(this, GithubRepository()) } } ࠗ۾ -
class GithubRepoActivity { override val presenter: GithubRepoPresenter by lazy {
GithubRepoPresenter(this, GithubRepository()) } } ࠗ۾ 6*ী䞵%BUBଵઑ -
class GithubRepoActivity { override val presenter: GithubRepoPresenter by lazy {
GithubRepoPresenter(this, GithubRepository()) } } ࠗ۾ 6*ী䞵%BUBଵઑ %* -
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 -
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 -
class IssueCreatePresenter( view: IssueCreateView, private val dataObserver: DataObserver, private val
repository: GithubRepository ) Good Case -
class IssueCreatePresenter( view: IssueCreateView, private val dataObserver: DataObserver, private val
repository: GithubRepository ) Good Case 4UBUJD$MBTTܳੌ߈$MBTT۽߸҃ ઓࢿੑ߉ -
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 -
interface GithubReposView { fun showNoInternetWarning() fun showLoading() fun hideLoading() fun
showRepos(repos: List<GithubRepo>) } fun onCreate() { if (networkHelper.isConnected().not()) view.showNoInternetWarning() } UI Method -
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 -
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 ాन -
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 -
ਗ 2 - উ٘۽٘ ઓ زਸ Wrapping ೠ 4%,.FUIPE6*%# "1*ాन
4%,.FUIPE8SBQQFS 7JFX*OUFSGBDF 3FQPTJUPSZ .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 -
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 -
override fun onCreate(savedInstanceState: Bundle?) { presenter.onCreate(it) } fun onCreate(repo: GithubRepo)
{ this.repo = repo view.showRepo(repo) issues(repo) } Good Case -
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प೯݅ -
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 - ചݶ ղ ۽ -
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 - ചݶ ղ ۽ ഐৈࠗపझࠛо -
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 -
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प೯݅ -
@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۽పझоמ -
@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ദٙ -
@RunWith(AndroidJUnit4::class) class GithubRepoActivityTest { @get:Rule val activityRule = ActivityTestRule(GithubRepoActivity::class.java) @Test
fun onCreate() { activityRule.launchActivity(null) } } "DUJWJUZദٙ -
- UI Test - DB Test Instrument Test 5FTUBCMF"SDIJUFDUVSFۄ䢞 7JFX
3FQPTJUPSZ݅୶о۽పझ -
1. ઓ Class ز - Mocking, Stub 2. উ٘۽٘ ઓ
ز - Wrapping, Mocking పझܳ য۵ѱ ೞח Ѫ -
1. ઓ Class ز - Mocking, Stub 2. উ٘۽٘ ઓ
ز - Wrapping, Mocking పझܳ য۵ѱ ೞח Ѫ 2"۽䟰ૐೞחѪࡅܰ Ҋוԑ పझܳ୶оೡदрহ -
1. ઓ Class ز - Mocking, Stub 2. উ٘۽٘ ઓ
ز - Wrapping, Mocking పझܳ য۵ѱ ೞח Ѫ పझബਊࢿਸݽܰѷ ੌցޖ߄ࡅزӝоঋח -
పझ ബਊࢿ 1. ز ഛੋ 2. ߸҃ ਬোࢿ ഛࠁ 3.
ܻ٘࠭ܳ ذח -
1. ز ഛੋ 2. ߸҃ ਬোࢿ ഛࠁ 3. ܻ٘࠭ܳ ذח
పझ ബਊࢿ పझחبܳݺदച పझܳঈೞҊ٘ܳࠁݶܻ࠭оࣻ䤰ೞ -
1. ܻನ۽ അपਸ ӵײ 2. ࣼ೧ӝө ъઁࢿਸ ف పझ زӝࠗৈ
ழߡܻ䥅ࠁ֫ঋਵݶ %POsU"QQSPWF -
1. ܻನ۽ അपਸ ӵײ 2. ࣼ೧ӝө ъઁࢿਸ ف 3. ए
Ѫࠗఠ ରӔରӔ పझ زӝࠗৈ 1SFTFOUFS7.5FTU 6OJU5FTU 1SFTFOUFS6OJUపझо ࣘبоࡅܰҊ ࣻݺӡ -
ਗ 1. ઓࢿ ёח ݽف ੑ߉ח 2. উ٘۽٘ ઓ زਸ
Wrapping ೠ 3. ݽٚ ز Presenterܳ ాೠ -
పझ زӝࠗৈ 5FTUBCMF$VMUVSF 1. ܻನ۽ അपਸ ӵײ 2. ࣼ೧ӝө ъઁࢿਸ
ف 3. ए Ѫࠗఠ ରӔରӔ