Kyash AndroidのUIテストを運用にのせるまで / How to introduce UI tests in Kyash Android

35e08efcf39d692f540047fb756eb4e3?s=47 konifar
September 11, 2018

Kyash AndroidのUIテストを運用にのせるまで / How to introduce UI tests in Kyash Android

35e08efcf39d692f540047fb756eb4e3?s=128

konifar

September 11, 2018
Tweet

Transcript

  1. Kyash Androidͷ UIςετΛӡ༻ʹͷͤ Δ·Ͱ 2018/09/11 (Ր) ώΧˑϥϘ Kyash Inc @konifar

  2. ઌ೔ɺKyash Android ͷUIͷ ςετ͕૸Γ࢝Ί·ͨ͠

  3. None
  4. UIͷςετΛೖΕͨཧ༝ • UnitςετͰर͍ʹ͍͘όά͕ൃੜͨ͠ • खಈͷςετέʔεΛ࡞͍͕ͬͯͨɺ֬ೝ͠ͳ ͍࣌ʹ͔͗ͬͯόά͸ى͖Δ • KPTʹͯʰςετΛࣗಈԽ͢Δʱͱ͍͏ Try͕ ग़ͯ΍ͬͯΈΔ͜ͱʹͨ͠

  5. ҙࣝͨ͜͠ͱ ׬ᘳΛ໨ࢦͭͭ͠ɺ·ͣӡ༻ʹͷͤΔ
 
 => ͞·͟·ͳબఆΛ͢Δ࣌ͷҙࢥܾఆʹӨڹ͢ ΔͷͰॏཁ

  6. ࠓ೔࿩͢͜ͱ 1. ςετπʔϧͷબఆ 2. ύοέʔδɾΫϥεͷ෼ׂࢦ਑ 3. ֎෦ͱͷ௨৴ͷࢦ਑ 4. CIαʔϏεͷબఆ

  7. ࠓ೔࿩͢͜ͱ 1. ςετπʔϧͷબఆ 2. ύοέʔδɾΫϥεͷ෼ׂࢦ਑ 3. ֎෦ͱͷ௨৴ͷࢦ਑ 4. CIαʔϏεͷબఆ

  8. Espresso • Robotium΍Appiumͱ͍ͬͨબ୒ࢶ΋͋Δ ͕ɺEspressoΛ࢖͏͜ͱʹͨ͠ • Support Libraryʹೖ͍ͬͯΔ͜ͱɺ؆ܿʹه ड़Ͱ͖Δ͜ͱɺઌਓͷ஌ݟ΋͋;Ε͍ͯΔ͜ ͱͳͲ͕ཧ༝

  9. Espressoͷૉ੖Β͍͠ࢿྉ • มߋʹڧ͍EspressoςετίʔυΛޮ཰ྑ͘ ॻ͜͏
 https://speakerdeck.com/sumio/droidkaigi2017-lets-write-sustainable- espresso-test-rapidly • EspressoςετίʔυͷಉظॲཧΛڀΊΔ
 https://speakerdeck.com/sumio/synchronization-capabilities-of-espresso

  10. ࠓ೔࿩͢͜ͱ 1. ςετπʔϧͷબఆ 2. ύοέʔδɾΫϥεͷ෼ׂࢦ਑ 3. ֎෦ͱͷ௨৴ͷࢦ਑ 4. CIαʔϏεͷબఆ

  11. ύοέʔδɾΫϥεͷ෼ׂ • Ͳ͜ʹԿΛॻ͍͍͍͔ͯ໌֬Ͱͳ͍ͱॻ͖ʹ ͍͘͠൙ཞ͕ͪ͠ • productionίʔυͱ͸ҧ͏ύοέʔδߏ੒ Ͱɺ࠷௿ݶͷࢦ਑ΛܾΊͨ

  12. /src/androidTest |--AndroidManifest.xml |--java | |--co | | |--kyash | |

    | |--AndroidTestApp.kt | | | |--di | | | | |--TestAnalyticsModule.kt | | | | |--TestNetModule.kt | | | | |--... | | | |--pageobject | | | | |--account | | | | | |--AccountSettingPageObject.kt | | | | | |--... | | | | |--card | | | | | |--AboutLinkedCardPageObject.kt | | | | | |--... | | | | | ... | | | | ... | | | |--scenario | | | | |--AddLinkedCardTest.kt | | | | |--LoginTest.kt | | | | |--SignInTest.kt | | | | |--... | | | |--testing | | | | |--AndroidTestUtils.kt | | | | |--CustomTestRunner.kt | | | | |--...
  13. di ύοέʔδ • KyashͰ͸Dagger2Λ࢖͍ͬͯΔ • ςετ࣮ߦ࣌ʹGoogleAnalytics΍APIΫϥΠ Ξϯτͷ࣮૷Λม͑ΔͨΊͷςετ༻ͷ ModuleΛஔ͘

  14. pageobject ύοέʔδ • PageObjectύλʔϯ
 https://martinfowler.com/bliki/PageObject.html • ֤ը໘Ͱඞཁͳૢ࡞΍ݕূΛ·ͱΊͨΫϥε Λஔ͘

  15. ϩάΠϯը໘ͷPageObject object LoginPageObject { ... fun inputEmail(text: String) = apply

    { onView(withId(R.id.email_edit).perform(scrollTo(), replaceText(text), closeSoftKeyboard()) } fun clickEmailLoginButton() = apply { onView(withId(R.id.button)).perform(scrollTo(), click()) }
  16. ϩάΠϯը໘ͷPageObject object LoginPageObject { ... fun inputEmail(text: String) = apply

    { onView(withId(R.id.email_edit).perform(scrollTo(), replaceText(text), closeSoftKeyboard()) } fun clickEmailLoginButton() = apply { onView(withId(R.id.button)).perform(scrollTo(), click()) } ϝʔϧΞυϨεͷೖྗ
  17. ϩάΠϯը໘ͷPageObject object LoginPageObject { ... fun inputEmail(text: String) = apply

    { onView(withId(R.id.email_edit).perform(scrollTo(), replaceText(text), closeSoftKeyboard()) } fun clickEmailLoginButton() = apply { onView(withId(R.id.button)).perform(scrollTo(), click()) } ϩάΠϯϘλϯͷԡԼ
  18. ϩάΠϯը໘ͷPageObject object LoginPageObject { ... fun inputEmail(text: String) = apply

    { onView(withId(R.id.email_edit).perform(scrollTo(), replaceText(text), closeSoftKeyboard()) } fun clickEmailLoginButton() = apply { onView(withId(R.id.button)).perform(scrollTo(), click()) } Kotlinͷapplyؔ਺ͰϝιουνΣʔϯʹ
  19. PageObject ͷݺͼग़͠ LoginPageObject .inputEmail("taro@kyash.co") .inputPassword("kyash123") .clickEmailLoginButton()

  20. PageObject ͷݺͼग़͠ LoginPageObject .inputEmail("taro@kyash.co") .inputPassword("kyash123") .clickEmailLoginButton() ← ϝʔϧΞυϨεΛೖྗ

  21. PageObject ͷݺͼग़͠ LoginPageObject .inputEmail("taro@kyash.co") .inputPassword("kyash123") .clickEmailLoginButton() ← ύεϫʔυΛೖྗ

  22. PageObject ͷݺͼग़͠ LoginPageObject .inputEmail("taro@kyash.co") .inputPassword("kyash123") .clickEmailLoginButton() ← ϩάΠϯϘλϯΛԡԼ

  23. PageObject ͷݺͼग़͠ LoginPageObject .inputEmail("taro@kyash.co") .inputPassword("kyash123") .clickEmailLoginButton() ݺͼग़͢ଆ͸Espressoͷίʔυʹґଘ͢Δ͜ͱͳ͘ ؆ܿͰಡΈ΍͍͢ςετίʔυ͕ॻ͚Δ

  24. scenario ύοέʔδ • ࣮ࡍʹJUnitςετΛ࣮ߦ͢ΔΫϥεΛஔ͘

  25. @LargeTest @RunWith(AndroidJUnit4::class) class LoginTest { @get:Rule var activityTestRule = ActivityTestRule(SplashActivity::class.java,

    true, false) .. /** * EmailͰϩάΠϯͯ͠΢ΥϨοτը໘Λ։͘·Ͱ */ @Test fun emailLogin() { SplashPageObject.launch(activityTestRule) WelcomePageObject .waitUntilShown() .clickLoginButton() LoginPageObject .inputEmail("taro@kyash.co") .inputPassword("kyash123") .clickEmailLoginButton() WalletPageObject.assertKyashCardInActiveExists() }
  26. @LargeTest @RunWith(AndroidJUnit4::class) class LoginTest { @get:Rule var activityTestRule = ActivityTestRule(SplashActivity::class.java,

    true, false) .. /** * EmailͰϩάΠϯͯ͠΢ΥϨοτը໘Λ։͘·Ͱ */ @Test fun emailLogin() { SplashPageObject.launch(activityTestRule) WelcomePageObject .waitUntilShown() .clickLoginButton() LoginPageObject .inputEmail("taro@kyash.co") .inputPassword("kyash123") .clickEmailLoginButton() WalletPageObject.assertKyashCardInActiveExists() } PageObjectΛհͯ͠ૢ࡞ɾݕূΛߦ͏
  27. @LargeTest @RunWith(AndroidJUnit4::class) class LoginTest { @get:Rule var activityTestRule = ActivityTestRule(SplashActivity::class.java,

    true, false) .. /** * EmailͰϩάΠϯͯ͠΢ΥϨοτը໘Λ։͘·Ͱ */ @Test fun emailLogin() { SplashPageObject.launch(activityTestRule) WelcomePageObject .waitUntilShown() .clickLoginButton() LoginPageObject .inputEmail("taro@kyash.co") .inputPassword("kyash123") .clickEmailLoginButton() WalletPageObject.assertKyashCardInActiveExists() } ը໘͕දࣔ͞ΕΔ·Ͱ଴ͭ
  28. ଴ͪ߹Θͤͷॲཧ fun waitUntilShown() = apply { val result = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())

    .wait(Until.hasObject(By.text(getString(R.string.hogehoge))), 5000) assertTrue(result) } sleep()͸࢖ΘͣɺUI AutomatorΛ࢖ͬͯ Կ͔͕දࣔ͞ΕΔ·Ͱ଴ͭ
  29. testing ύοέʔδ • EspressoͰUIͷςετΛॻ্͘ͰඞཁͳUtility ΍MatcherΛஔ͘ • ࠓͷͱ͜Ζ໰୊ͳ͍͕ɺࠓޙΫϥε͕ଟ͘ͳͬ ͖ͯͨΒ΋͏গ͠෼ׂ͢Δ͔΋

  30. ࠓ೔࿩͢͜ͱ 1. ςετπʔϧͷબఆ 2. ύοέʔδɾΫϥεͷ෼ׂࢦ਑ 3. ֎෦ͱͷ௨৴ͷࢦ਑ 4. CIαʔϏεͷબఆ

  31. 3ͭͷબ୒ࢶ 1. ςετ༻ͷαʔόʔΛ༻ҙͯͭ͠ͳ͙ 2. Mock Web ServerΛ࢖͏ 3. APIΫϥΠΞϯτΛϞοΫʹஔ͖׵͑Δ

  32. 1. ςετ༻ͷαʔόʔΛ༻ҙ͠ ͯͭͳ͙ ςετ"1*αʔόʔ "1*ΫϥΠΞϯτ γφϦΦςετ json

  33. 1. ςετ༻ͷαʔόʔΛ༻ҙ͠ ͯͭͳ͙ • ࣮ࡍʹૢ࡞͢Δͷͱ΄΅ಉ͡৚݅ͰςετͰ ͖ΔͨΊɺAPIͷ࢓༷͕༧ظͤͣมߋ͞Εͨ৔ ߹΍ɺΤϥʔ͕ஔ͖͍ͯΔ৔߹΋ݕग़Ͱ͖Δ • ࣮ߦ࣌ʹৗʹಉ͡ঢ়ଶʹͨ͠Γಉ࣌ʹ࣮ߦ͞ ΕͨΓͨ͠৔߹Λߟྀ͢Δͱɺςετ࣮ߦ͝

    ͱʹdockerΛ্ཱͪ͛Δ౳ͷ޻෉͕ඞཁ
  34. 2. Mock Web ServerΛ࢖͏ ςετ"1*αʔόʔ "1*ΫϥΠΞϯτ γφϦΦςετ .PDL8FC4FSWFS mock json

  35. 2. Mock Web ServerΛ࢖͏ • OkHttpͷmockwebserverΛ࢖͑͹ɺൺֱత ಋೖ͕༰қɻϓϩδΣΫτ಺ͷίʔυͰ׬݁ ͢Δ෼ɺ1ʹൺ΂ͯӡ༻͠΍͍͢ • ϞοΫϨεϙϯεͷjsonΛ༻ҙ͓͔ͯ͠ͳ͚

    Ε͹ͳΒͳ͍෼ɺख͕͔͔ؒΔ͠APIଆͷมߋ ΍௥Ճʹ௥ैͰ͖ͳ͘ͳΔՄೳੑ΋͋Δ
  36. 3. APIΫϥΠΞϯτΛϞοΫʹ ஔ͖׵͑Δ ςετ"1*αʔόʔ "1*ΫϥΠΞϯτ γφϦΦςετ .PDL8FC4FSWFS mock object ϞοΫ

    "1*ΫϥΠΞϯτ
  37. 3. APIΫϥΠΞϯτΛϞοΫʹ ஔ͖׵͑Δ • αʔόʔ΍APIΫϥΠΞϯτ͸ظ଴௨ΓͷৼΔ ෣͍Λ͢ΔલఏͰɺͦΕΑΓ্ͷϨΠϠʔΛ Ϣχοτςετͱಉ͡Α͏ʹςετͰ͖Δͷ Ͱଞͷ2ͭͱൺ΂ͯϝϯςφϯε͠΍͍͢ • APIͷjsonͱύʔεͰόά͕͋ͬͨ৔߹ʹ͸ؾ

    ͚ͮͳ͍
  38. KyashͰ͸ʰ3. APIΫϥΠΞϯτ ΛϞοΫʹஔ͖׵͑ΔʱΛબ୒ • ࠓ·Ͱόά͕ى͖͍ͯͨ෦෼͸3ͷ΍ΓํͰ΋ र͑ͦ͏ͩͬͨ • ·ͣ͸ӡ༻ʹͷͤΔͱ͜Ζ·Ͱ΍Γ͔ͨͬͨ • ࠓޙςεταʔόʔΛཱͯΔʹͯ͠΋ɺঢ়گ

    ʹԠͯ͡ϞοΫ͢Δํࣜͱڞଘ͍ͯ͘͜͠ͱ ʹͳΓͦ͏
  39. @LargeTest @RunWith(AndroidJUnit4::class) class LoginTest { @get:Rule var activityTestRule = ActivityTestRule(SplashActivity::class.java,

    true, false) @Before fun setUp() { val facebookSdkUtil = WelcomePageObject.mockFacebookSdk() val kyashApi = mock<KyashApi>().apply { SplashPageObject.mockApi(this) SignInPageObject.mockApi(this) MainPageObject.mockApiForEmptyData(this) } val app = AndroidTestUtils.getApp() app.reloadDagger(kyashApi, facebookSdkUtil) app.logout() }
  40. @LargeTest @RunWith(AndroidJUnit4::class) class LoginTest { @get:Rule var activityTestRule = ActivityTestRule(SplashActivity::class.java,

    true, false) @Before fun setUp() { val facebookSdkUtil = WelcomePageObject.mockFacebookSdk() val kyashApi = mock<KyashApi>().apply { SplashPageObject.mockApi(this) SignInPageObject.mockApi(this) MainPageObject.mockApiForEmptyData(this) } val app = AndroidTestUtils.getApp() app.reloadDagger(kyashApi, facebookSdkUtil) app.logout() } PageObject͝ͱʹAPIΛϞοΫ͢Δ
  41. @LargeTest @RunWith(AndroidJUnit4::class) class LoginTest { @get:Rule var activityTestRule = ActivityTestRule(SplashActivity::class.java,

    true, false) @Before fun setUp() { val facebookSdkUtil = WelcomePageObject.mockFacebookSdk() val kyashApi = mock<KyashApi>().apply { SplashPageObject.mockApi(this) SignInPageObject.mockApi(this) MainPageObject.mockApiForEmptyData(this) } val app = AndroidTestUtils.getApp() app.reloadDagger(kyashApi, facebookSdkUtil) app.logout() } ϞοΫͨ͠APIΫϥΠΞϯτΛ౉ͯ͠daggerΛॳظԽ
  42. ࠓ೔࿩͢͜ͱ 1. ςετπʔϧͷબఆ 2. ύοέʔδɾΫϥεͷ෼ׂࢦ਑ 3. ֎෦ͱͷ௨৴ͷࢦ਑ 4. CIαʔϏεͷબఆ

  43. CIαʔϏεͷબఆ • Firebase Test LabΛ࢖ͬͯςετΛ࣮ߦ
 https://firebase.google.com/docs/test-lab/overview • CircleCIɺBitriseɺTravisCIɺJenkins ԿͰ΋ Α͔ͬͨͷͰɺಋೖָ͕ͦ͏ͳ΋ͷΛબ୒

  44. BitriseΛબ୒ • αʔόʔͷCI΋ڞଘ͍ͯ͠ΔCircleCIͷΩϡʔʹ࣌ؒ ͷ͔͔ΔδϣϒΛੵΈͨ͘ͳ͍ • iOS appͰ͸BitriseΛ࢖͍ͬͯͯ՝ۚ΋͍ͯ͠Δ • (ଞͱൺֱͯ͠) ఆظ࣮ߦͷઃఆΛGUIͰ؆୯ʹͰ͖Δ

    • Virtual Device Testing for AndroidͰFirebase Test Labͱͷ࿈ܞ΋GUIͰ؆୯ʹͰ͖Δ
  45. Virtual Device Testing for Android • Bitriseͷbeta൛ͷStep • GUI্ͰઃఆΛॻ͍࣮ͯߦ͢Δ͚ͩͰFirebase Test

    LabͰUIςετΛ࣮ߦͰ͖Δ
  46. None
  47. VIRTUAL DEVICE TESTS λϒ͕௥Ճ͞ΕΔ

  48. ςετ݁Ռ͕ݟΕΔ

  49. ςετதͷಈը΋ݟΕΔ

  50. ࣮ߦλΠϛϯά • ݱঢ়15෼͘Β͍͔͔ΔͷͰɺpush΍PR͝ͱͷ ࣮ߦ͸͍ͯ͠ͳ͍ • ຖ೔0࣌ʹఆظ࣮ߦ + ϦϦʔεϒϥϯνʹϚʔ δ͞ΕͨλΠϛϯάͰ࣮ߦ

  51. ·ͱΊ

  52. ςετπʔϧͷબఆ • ҆ఆͷEspressoΛબ୒ • ૉ੖Β͍͠ࢿྉ͕͋ΔͷͰಡ΋͏
 https://speakerdeck.com/sumio/droidkaigi2017-lets-write-sustainable- espresso-test-rapidly
 https://speakerdeck.com/sumio/synchronization-capabilities-of-espresso

  53. ύοέʔδɾΫϥεͷ෼ׂࢦ਑ • productionͱ͸ҧ͏ύοέʔδߏ੒ • PageObjectΛ࡞ͬͯը໘͝ͱͷૢ࡞΍ݕূΛ ·ͱΊΔ • JUnitͷςετ͸scenarioύοέʔδҎԼʹஔ ͖ɺPageObjectΛհͯ͠ςετΛ૊ΈཱͯΔ

  54. ֎෦ͱͷ௨৴ͷࢦ਑ • 3ͭͷબ୒ࢶ͕͋ͬͨ • ݕূ͍ͨ͜͠ͱ΍ಋೖͷ༰қ͞Λߟྀ͠ɺAPI ΫϥΠΞϯτΛϞοΫ͢Δํ๏Λબ୒

  55. CIαʔϏεͷબఆ • ఆظ࣮ߦ΍UIςετͷઃఆ͕ൺֱత༰қͳ BitriseΛબఆ • ຖ೔0࣌ʹఆظ࣮ߦ + ϦϦʔεϒϥϯνʹϚʔ δ͞ΕͨλΠϛϯάͰ࣮ߦ

  56. 10෼Ͱ࿩ͤͳ͍͜ͱ΋ଟ͍ ͷͰɺৄ͍͠࿩͸࠙਌ձͰ

  57. ͋Γ͕ͱ͏͍͟͝·ͨ͠