Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Speaker Deck
PRO
Sign in
Sign up
for free
Kyash AndroidのUIテストを運用にのせるまで / How to introduce UI tests in Kyash Android
konifar
September 11, 2018
Programming
0
820
Kyash AndroidのUIテストを運用にのせるまで / How to introduce UI tests in Kyash Android
https://atnd.org/events/99344
konifar
September 11, 2018
Tweet
Share
More Decks by konifar
See All by konifar
konifar
3
1.1k
konifar
2
680
konifar
23
10k
konifar
2
220
konifar
2
630
konifar
5
290
konifar
14
16k
konifar
3
650
konifar
8
36k
Other Decks in Programming
See All in Programming
samjulien
0
400
timeseriesfr
0
120
kaz29
2
130
n1215
1
480
seike460
8
2.2k
oracle4engineer
0
150
fei0203018
0
110
bkuhlmann
6
710
hiro_y
1
470
dunglas
5
5.6k
palkan
2
370
kodetr
0
150
Featured
See All Featured
stephaniewalter
262
11k
imathis
479
150k
jakevdp
776
200k
moore
125
21k
holman
288
130k
keavy
107
14k
shlominoach
176
7.6k
tammielis
237
23k
carmenhchung
34
1.6k
bryan
100
11k
skipperchong
8
790
jeffersonlam
330
15k
Transcript
Kyash Androidͷ UIςετΛӡ༻ʹͷͤ Δ·Ͱ 2018/09/11 (Ր) ώΧˑϥϘ Kyash Inc @konifar
ઌɺKyash Android ͷUIͷ ςετ͕Γ࢝Ί·ͨ͠
None
UIͷςετΛೖΕͨཧ༝ • UnitςετͰर͍ʹ͍͘όά͕ൃੜͨ͠ • खಈͷςετέʔεΛ࡞͍͕ͬͯͨɺ֬ೝ͠ͳ ͍࣌ʹ͔͗ͬͯόάى͖Δ • KPTʹͯʰςετΛࣗಈԽ͢Δʱͱ͍͏ Try͕ ग़ͯͬͯΈΔ͜ͱʹͨ͠
ҙࣝͨ͜͠ͱ ᘳΛࢦͭͭ͠ɺ·ͣӡ༻ʹͷͤΔ => ͞·͟·ͳબఆΛ͢Δ࣌ͷҙࢥܾఆʹӨڹ͢ ΔͷͰॏཁ
ࠓ͢͜ͱ 1. ςετπʔϧͷબఆ 2. ύοέʔδɾΫϥεͷׂࢦ 3. ֎෦ͱͷ௨৴ͷࢦ 4. CIαʔϏεͷબఆ
ࠓ͢͜ͱ 1. ςετπʔϧͷબఆ 2. ύοέʔδɾΫϥεͷׂࢦ 3. ֎෦ͱͷ௨৴ͷࢦ 4. CIαʔϏεͷબఆ
Espresso • RobotiumAppiumͱ͍ͬͨબࢶ͋Δ ͕ɺEspressoΛ͏͜ͱʹͨ͠ • Support Libraryʹೖ͍ͬͯΔ͜ͱɺ؆ܿʹه ड़Ͱ͖Δ͜ͱɺઌਓͷݟ͋;Ε͍ͯΔ͜ ͱͳͲ͕ཧ༝
EspressoͷૉΒ͍͠ࢿྉ • มߋʹڧ͍EspressoςετίʔυΛޮྑ͘ ॻ͜͏ https://speakerdeck.com/sumio/droidkaigi2017-lets-write-sustainable- espresso-test-rapidly • EspressoςετίʔυͷಉظॲཧΛڀΊΔ https://speakerdeck.com/sumio/synchronization-capabilities-of-espresso
ࠓ͢͜ͱ 1. ςετπʔϧͷબఆ 2. ύοέʔδɾΫϥεͷׂࢦ 3. ֎෦ͱͷ௨৴ͷࢦ 4. CIαʔϏεͷબఆ
ύοέʔδɾΫϥεͷׂ • Ͳ͜ʹԿΛॻ͍͍͍͔ͯ໌֬Ͱͳ͍ͱॻ͖ʹ ͍͘͠൙ཞ͕ͪ͠ • productionίʔυͱҧ͏ύοέʔδߏ Ͱɺ࠷ݶͷࢦΛܾΊͨ
/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 | | | | |--...
di ύοέʔδ • KyashͰDagger2Λ͍ͬͯΔ • ςετ࣮ߦ࣌ʹGoogleAnalyticsAPIΫϥΠ Ξϯτͷ࣮Λม͑ΔͨΊͷςετ༻ͷ ModuleΛஔ͘
pageobject ύοέʔδ • PageObjectύλʔϯ https://martinfowler.com/bliki/PageObject.html • ֤ը໘Ͱඞཁͳૢ࡞ݕূΛ·ͱΊͨΫϥε Λஔ͘
ϩάΠϯը໘ͷ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()) }
ϩάΠϯը໘ͷ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()) } ϝʔϧΞυϨεͷೖྗ
ϩάΠϯը໘ͷ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()) } ϩάΠϯϘλϯͷԡԼ
ϩάΠϯը໘ͷ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ؔͰϝιουνΣʔϯʹ
PageObject ͷݺͼग़͠ LoginPageObject .inputEmail("taro@kyash.co") .inputPassword("kyash123") .clickEmailLoginButton()
PageObject ͷݺͼग़͠ LoginPageObject .inputEmail("taro@kyash.co") .inputPassword("kyash123") .clickEmailLoginButton() ← ϝʔϧΞυϨεΛೖྗ
PageObject ͷݺͼग़͠ LoginPageObject .inputEmail("taro@kyash.co") .inputPassword("kyash123") .clickEmailLoginButton() ← ύεϫʔυΛೖྗ
PageObject ͷݺͼग़͠ LoginPageObject .inputEmail("taro@kyash.co") .inputPassword("kyash123") .clickEmailLoginButton() ← ϩάΠϯϘλϯΛԡԼ
PageObject ͷݺͼग़͠ LoginPageObject .inputEmail("taro@kyash.co") .inputPassword("kyash123") .clickEmailLoginButton() ݺͼग़͢ଆEspressoͷίʔυʹґଘ͢Δ͜ͱͳ͘ ؆ܿͰಡΈ͍͢ςετίʔυ͕ॻ͚Δ
scenario ύοέʔδ • ࣮ࡍʹJUnitςετΛ࣮ߦ͢ΔΫϥεΛஔ͘
@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() }
@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Λհͯ͠ૢ࡞ɾݕূΛߦ͏
@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() } ը໘͕දࣔ͞ΕΔ·Ͱͭ
ͪ߹Θͤͷॲཧ fun waitUntilShown() = apply { val result = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
.wait(Until.hasObject(By.text(getString(R.string.hogehoge))), 5000) assertTrue(result) } sleep()ΘͣɺUI AutomatorΛͬͯ Կ͔͕දࣔ͞ΕΔ·Ͱͭ
testing ύοέʔδ • EspressoͰUIͷςετΛॻ্͘ͰඞཁͳUtility MatcherΛஔ͘ • ࠓͷͱ͜Ζͳ͍͕ɺࠓޙΫϥε͕ଟ͘ͳͬ ͖ͯͨΒ͏গׂ͢͠Δ͔
ࠓ͢͜ͱ 1. ςετπʔϧͷબఆ 2. ύοέʔδɾΫϥεͷׂࢦ 3. ֎෦ͱͷ௨৴ͷࢦ 4. CIαʔϏεͷબఆ
3ͭͷબࢶ 1. ςετ༻ͷαʔόʔΛ༻ҙͯͭ͠ͳ͙ 2. Mock Web ServerΛ͏ 3. APIΫϥΠΞϯτΛϞοΫʹஔ͖͑Δ
1. ςετ༻ͷαʔόʔΛ༻ҙ͠ ͯͭͳ͙ ςετ"1*αʔόʔ "1*ΫϥΠΞϯτ γφϦΦςετ json
1. ςετ༻ͷαʔόʔΛ༻ҙ͠ ͯͭͳ͙ • ࣮ࡍʹૢ࡞͢Δͷͱ΄΅ಉ݅͡ͰςετͰ ͖ΔͨΊɺAPIͷ༷͕༧ظͤͣมߋ͞Εͨ ߹ɺΤϥʔ͕ஔ͖͍ͯΔ߹ݕग़Ͱ͖Δ • ࣮ߦ࣌ʹৗʹಉ͡ঢ়ଶʹͨ͠Γಉ࣌ʹ࣮ߦ͞ ΕͨΓͨ͠߹Λߟྀ͢Δͱɺςετ࣮ߦ͝
ͱʹdockerΛ্ཱͪ͛Δͷ͕ඞཁ
2. Mock Web ServerΛ͏ ςετ"1*αʔόʔ "1*ΫϥΠΞϯτ γφϦΦςετ .PDL8FC4FSWFS mock json
2. Mock Web ServerΛ͏ • OkHttpͷmockwebserverΛ͑ɺൺֱత ಋೖ͕༰қɻϓϩδΣΫτͷίʔυͰ݁ ͢Δɺ1ʹൺͯӡ༻͍͢͠ • ϞοΫϨεϙϯεͷjsonΛ༻ҙ͓͔ͯ͠ͳ͚
ΕͳΒͳ͍ɺख͕͔͔ؒΔ͠APIଆͷมߋ ՃʹैͰ͖ͳ͘ͳΔՄೳੑ͋Δ
3. APIΫϥΠΞϯτΛϞοΫʹ ஔ͖͑Δ ςετ"1*αʔόʔ "1*ΫϥΠΞϯτ γφϦΦςετ .PDL8FC4FSWFS mock object ϞοΫ
"1*ΫϥΠΞϯτ
3. APIΫϥΠΞϯτΛϞοΫʹ ஔ͖͑Δ • αʔόʔAPIΫϥΠΞϯτظ௨ΓͷৼΔ ͍Λ͢ΔલఏͰɺͦΕΑΓ্ͷϨΠϠʔΛ Ϣχοτςετͱಉ͡Α͏ʹςετͰ͖Δͷ Ͱଞͷ2ͭͱൺͯϝϯςφϯε͍͢͠ • APIͷjsonͱύʔεͰόά͕͋ͬͨ߹ʹؾ
͚ͮͳ͍
KyashͰʰ3. APIΫϥΠΞϯτ ΛϞοΫʹஔ͖͑ΔʱΛબ • ࠓ·Ͱόά͕ى͖͍ͯͨ෦3ͷΓํͰ र͑ͦ͏ͩͬͨ • ·ͣӡ༻ʹͷͤΔͱ͜Ζ·ͰΓ͔ͨͬͨ • ࠓޙςεταʔόʔΛཱͯΔʹͯ͠ɺঢ়گ
ʹԠͯ͡ϞοΫ͢Δํࣜͱڞଘ͍ͯ͘͜͠ͱ ʹͳΓͦ͏
@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() }
@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ΛϞοΫ͢Δ
@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ΛॳظԽ
ࠓ͢͜ͱ 1. ςετπʔϧͷબఆ 2. ύοέʔδɾΫϥεͷׂࢦ 3. ֎෦ͱͷ௨৴ͷࢦ 4. CIαʔϏεͷબఆ
CIαʔϏεͷબఆ • Firebase Test LabΛͬͯςετΛ࣮ߦ https://firebase.google.com/docs/test-lab/overview • CircleCIɺBitriseɺTravisCIɺJenkins ԿͰ Α͔ͬͨͷͰɺಋೖָ͕ͦ͏ͳͷΛબ
BitriseΛબ • αʔόʔͷCIڞଘ͍ͯ͠ΔCircleCIͷΩϡʔʹ࣌ؒ ͷ͔͔ΔδϣϒΛੵΈͨ͘ͳ͍ • iOS appͰBitriseΛ͍ͬͯͯ՝͍ۚͯ͠Δ • (ଞͱൺֱͯ͠) ఆظ࣮ߦͷઃఆΛGUIͰ؆୯ʹͰ͖Δ
• Virtual Device Testing for AndroidͰFirebase Test Labͱͷ࿈ܞGUIͰ؆୯ʹͰ͖Δ
Virtual Device Testing for Android • Bitriseͷbeta൛ͷStep • GUI্ͰઃఆΛॻ͍࣮ͯߦ͢Δ͚ͩͰFirebase Test
LabͰUIςετΛ࣮ߦͰ͖Δ
None
VIRTUAL DEVICE TESTS λϒ͕Ճ͞ΕΔ
ςετ݁Ռ͕ݟΕΔ
ςετதͷಈըݟΕΔ
࣮ߦλΠϛϯά • ݱঢ়15͘Β͍͔͔ΔͷͰɺpushPR͝ͱͷ ࣮ߦ͍ͯ͠ͳ͍ • ຖ0࣌ʹఆظ࣮ߦ + ϦϦʔεϒϥϯνʹϚʔ δ͞ΕͨλΠϛϯάͰ࣮ߦ
·ͱΊ
ςετπʔϧͷબఆ • ҆ఆͷEspressoΛબ • ૉΒ͍͠ࢿྉ͕͋ΔͷͰಡ͏ https://speakerdeck.com/sumio/droidkaigi2017-lets-write-sustainable- espresso-test-rapidly https://speakerdeck.com/sumio/synchronization-capabilities-of-espresso
ύοέʔδɾΫϥεͷׂࢦ • productionͱҧ͏ύοέʔδߏ • PageObjectΛ࡞ͬͯը໘͝ͱͷૢ࡞ݕূΛ ·ͱΊΔ • JUnitͷςετscenarioύοέʔδҎԼʹஔ ͖ɺPageObjectΛհͯ͠ςετΛΈཱͯΔ
֎෦ͱͷ௨৴ͷࢦ • 3ͭͷબࢶ͕͋ͬͨ • ݕূ͍ͨ͜͠ͱಋೖͷ༰қ͞Λߟྀ͠ɺAPI ΫϥΠΞϯτΛϞοΫ͢Δํ๏Λબ
CIαʔϏεͷબఆ • ఆظ࣮ߦUIςετͷઃఆ͕ൺֱత༰қͳ BitriseΛબఆ • ຖ0࣌ʹఆظ࣮ߦ + ϦϦʔεϒϥϯνʹϚʔ δ͞ΕͨλΠϛϯάͰ࣮ߦ
10Ͱͤͳ͍͜ͱଟ͍ ͷͰɺৄ͍࠙͠ձͰ
͋Γ͕ͱ͏͍͟͝·ͨ͠