Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
Kyash AndroidのUIテストを運用にのせるまで / How to introduce...
Search
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
konifar
September 11, 2018
Programming
1.2k
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Kyash AndroidのUIテストを運用にのせるまで / How to introduce UI tests in Kyash Android
https://atnd.org/events/99344
konifar
September 11, 2018
More Decks by konifar
See All by konifar
自分が "コントロールできる" と思い込んで "引き出しを増やす"
konifar
15
7k
自分が詳しくない領域でAIを使う #プロヒス2026
konifar
23
9.8k
マネージャー版 "提案のレベル" を上げる
konifar
27
22k
AIで 浮いた時間で 何をする? 2026春 #devsumi
konifar
21
6.6k
登壇資料を作る時に意識していること #登壇資料_findy
konifar
7
3.5k
事業部のプロジェクト進行と開発チームの改善の “時間軸" のすり合わせ
konifar
12
4.8k
チーム開発の “地ならし"
konifar
8
18k
AIで 浮いた時間で 何をする? #プロヒス2025
konifar
27
17k
物語を動かす行動"量" #エンジニアニメ
konifar
19
7.7k
Other Decks in Programming
See All in Programming
Everything will be SERVERLESS — 信じて運用した10年の経験値 / Everything Will be Serverless — Lessons Learned from 10 Years of Operational Experience
seike460
PRO
1
1.1k
Agents on Rails - Rails at Scale 2026
irinanazarova
0
560
動作中のプログラムの中身をリアルタイムに覗く / Realtime Debugger for CSharp with Roslyn
prota
1
1.9k
設計次第でAIコードの読む量は減らせる / designing-for-code-reading
minodriven
35
17k
【加筆修正版】Laravel のアプリケーションをどこにデプロイするか #phpcon_ehime
akase244
0
150
When benchmarks go bad - what I learned from measuring performance wrong
hollycummins
0
220
Railsのためのパスキー実践入門 〜マルチテナントSaaSでの意思決定〜
utakaha
2
540
コードレビューのボトルネックを"する側"と"される側"の両面から解消する
yub0n
2
1.6k
Jetpack Compose Mechanisms
skydoves
3
820
[Rails World 2026] Durable orchestration on Rails: from continuation to workflow
palkan
1
750
[Dev Group Paris Octobre 26] Agentforce, SIforce, Claudeforce, Slackbot… on s'y retrouve ?
nabondance
0
110
Findy - エンジニア向け会社紹介/Findy Company Deck
findyinc
6
400k
Featured
See All Featured
Improving Core Web Vitals using Speculation Rules API
sergeychernyshev
21
1.7k
The Illustrated Guide to Node.js - THAT Conference 2024
reverentgeek
1
570
GraphQLとの向き合い方2022年版
quramy
50
15k
Building a Modern Day E-commerce SEO Strategy
aleyda
45
9.3k
WENDY [Excerpt]
tessaabrams
14
40k
How to build a perfect <img>
jonoalderson
1
6.1k
AI: The stuff that nobody shows you
jnunemaker
PRO
10
1.2k
SEO in 2025: How to Prepare for the Future of Search
ipullrank
3
3.9k
A Tale of Four Properties
chriscoyier
163
24k
Lightning talk: Run Django tests with GitHub Actions
sabderemane
0
300
Self-Hosted WebAssembly Runtime for Runtime-Neutral Checkpoint/Restore in Edge–Cloud Continuum
chikuwait
0
890
Winning Ecommerce Organic Search in an AI Era - #searchnstuff2025
aleyda
2
2.2k
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("
[email protected]
") .inputPassword("kyash123") .clickEmailLoginButton()
PageObject ͷݺͼग़͠ LoginPageObject .inputEmail("
[email protected]
") .inputPassword("kyash123") .clickEmailLoginButton() ← ϝʔϧΞυϨεΛೖྗ
PageObject ͷݺͼग़͠ LoginPageObject .inputEmail("
[email protected]
") .inputPassword("kyash123") .clickEmailLoginButton() ← ύεϫʔυΛೖྗ
PageObject ͷݺͼग़͠ LoginPageObject .inputEmail("
[email protected]
") .inputPassword("kyash123") .clickEmailLoginButton() ← ϩάΠϯϘλϯΛԡԼ
PageObject ͷݺͼग़͠ LoginPageObject .inputEmail("
[email protected]
") .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("
[email protected]
") .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("
[email protected]
") .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("
[email protected]
") .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Ͱͤͳ͍͜ͱଟ͍ ͷͰɺৄ͍࠙͠ձͰ
͋Γ͕ͱ͏͍͟͝·ͨ͠