Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
3年以上運用しているアプリにUIテストを導入した #orecon_ios #fastlane_study_jp /orecon_ios_ui_test_20171003
Search
fromkk
October 03, 2017
Programming
2
1.3k
3年以上運用しているアプリにUIテストを導入した #orecon_ios #fastlane_study_jp /orecon_ios_ui_test_20171003
俺コン Vol.1 Day.2 にて発表した3年以上運用しているアプリにUIテストを導入した話です。
https://orecon.connpass.com/event/64285/
fromkk
October 03, 2017
Tweet
Share
More Decks by fromkk
See All by fromkk
note社の全員野球で品質向上活動について / note_qa_challenge #iOS_test_teatime
fromkk
2
1.7k
1年分のデータが見たいと言われてやったこと/yearly_data_with_note
fromkk
0
840
note iOSチームの自動化 ver.2021/automation_with_iOS_team_on_note_ver2021
fromkk
0
1.8k
Bitrise体験会説明資料/bitrise_explore
fromkk
1
990
noteのiOSアプリで実装したアクセシビリティの全て #iosdc #a /a11y_with_iOS_App_on_note
fromkk
2
3.1k
dSYMのアップロードで SPMを活用する/use_spm_with_upload_dsyms
fromkk
1
2.5k
Bitriseのリモートアクセス機能 #bitrise_meetup/remote_access_of_bitrise
fromkk
1
500
note社でのMagic Pod活用事例 #af_iosdc/magicpod_with_note
fromkk
2
10k
iOSには無いmacOS独自機能をCatalystで実装する #iosdc #d/make_macos_apps_with_catalyst
fromkk
9
2k
Other Decks in Programming
See All in Programming
しくじり先生 Image Matching Challenge 2024 編
goosehaaan
0
810
Webエディタライブラリ 「CodeMirror」から学ぶ Webアプリ開発のテクニック
ryosukeigarashi
0
250
最近追加した型の紹介とその振り返り
aki19035vc
0
180
Rubyのパフォーマンスプロファイリングの改善 / Enhancing performance profiling for Ruby
osyoyu
1
410
CSC307 Lecture 10
javiergs
PRO
0
310
From Spring Boot 2 to Spring Boot 3 with Java 22 and Jakarta EE
ivargrimstad
0
1.9k
君たちはどうコードをレビューする (される) か / 大吉祥寺.pm
utgwkk
15
8.5k
入社1ヶ月でここまでやった!Findy Toolsインフラ支援の最適化
rvirus0817
6
1.4k
3 Effective Rules for Success with Signals in Angular
manfredsteyer
PRO
0
120
【Go言語】ジェネリクス
tomo1227
0
170
CSC307 Lecture 13
javiergs
PRO
0
150
ドメイン駆動設計の実践
masuda220
PRO
19
5.2k
Featured
See All Featured
Fontdeck: Realign not Redesign
paulrobertlloyd
79
5.1k
WebSockets: Embracing the real-time Web
robhawkes
59
7.2k
A Modern Web Designer's Workflow
chriscoyier
689
190k
How GitHub Uses GitHub to Build GitHub
holman
471
290k
Large-scale JavaScript Application Architecture
addyosmani
506
110k
Design and Strategy: How to Deal with People Who Don’t "Get" Design
morganepeng
121
18k
[Rails World 2023 - Day 1 Closing Keynote] - The Magic of Rails
eileencodes
17
1.5k
GraphQLとの向き合い方2022年版
quramy
36
13k
Robots, Beer and Maslow
schacon
PRO
157
8.1k
Why You Should Never Use an ORM
jnunemaker
PRO
51
8.9k
Visualizing Your Data: Incorporating Mongo into Loggly Infrastructure
mongodb
36
9.1k
I Don’t Have Time: Getting Over the Fear to Launch Your Podcast
jcasabona
26
1.8k
Transcript
3Ҏ্ӡ༻͍ͯ͠ΔΞϓϦʹUIςετΛಋೖͨ͠ iOSDC Japan 2017 Զίϯ Vol.1 / Day. 2 1
Profile Kazuya Ueoka Timers inc.ͷiOSΤϯδχΞ Twitter: @fromkk Github: fromkk Qiita:
fromkk 2
3
4
5
ΤϯδχΞืूத 6
ࠓճUIςετΛಋೖͨ͠ΞϓϦ 7
8
Famm • 2014/05ϦϦʔεͷՈ͚ΫϩʔζυSNSΞϓϦ • iOS/AndroidରԠ • ຖ݄ϑΥτΧϨϯμʔΛҹͯ͠Ϣʔβʔʹಧ͚Δ 9
UIςετৼΓฦΓ 10
WWDC 2015 • UIཁૉΛݕࡧɾΞΫγϣϯΛ࣮ߦ • UIͷϓϩύςΟͱঢ়ଶΛݕূ • UI recording •
ςετ݁ՌΛϨϙʔτ 11
ʊਓਓਓਓਓਓਓਓਓਓਓਓਓਓਓʊ ʼɹԿ͍͍͔͔ͯ͠Βͳ͍ ɹʻ ʉY^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Yʉ 12
UIςετͷਏΈ • ͍͖ͳΓॳظը໘͕ىಈ͢Δ • ຖճཁૉΛݕࡧ͢Δͷ͕໘ • ͍ 13
UIςετΛಋೖ͢ΔʹͨͬͯఘΊͨࣄ 14
طଘը໘ͷςετΛॻ͘ࣄఘΊͨ • طଘίʔυ͕ີ݁߹ա͗ͨ(ViewControllerͰϞσϧ Λݺͼग़ͨ͠ΓURLʹΞΫηεͨ͠Γ...) • ৽نը໘ɾཁૉʹ accessibilityIdentifier Λؤுͬ ͯৼΔ༷ʹҙࣝ 15
ͬͨࣄ ͦͷ1 16
ΞϓϦىಈ࣌ʹจࣈྻͰViewControllerΛ ࢦఆͯ͠ىಈग़དྷΔ༷ʹͨ͠ • iOSDC 2016ͷ @dealforest ͞ΜͷൃදΛࢀߟ 17
ॳظը໘ΛStoryboard͔Β։͘ઃఆΛআ ↓ 18
ViewController༻ͱΦϓγϣϯ༻ͷΩʔΛܾΊ͓ͯ͘ struct LaunchKeys { static let viewController: String = "LAUNCH_VIEW_CONTROLLER"
static let userInfo: String = "LAUNCH_USER_INFO" } 19
εΩʔϜΛฤू ઌ΄ͲܾΊͨΩʔʹରͯ͠Λઃఆ͢Δ 20
Creatable protocol protocol Creatable { func create<T>(_ identifier: String, userInfo:
[AnyHashable: Any]?) -> T? } 21
Adopt to Creatable struct Creator: Creatable { func create<T>(_ identifier:
String, userInfo: [AnyHashable : Any]?) -> T? { switch identifier { case "ViewController": return viewController() as? T default: return nil } } } extension Creator { func viewController() -> ViewController { let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main) return storyboard.instantiateInitialViewController() as! ViewController } } 22
Launcher struct Launcher { var creator: Creatable init(with creator: Creatable)
{ self.creator = creator } func launch<T>() -> T? { guard let viewControllerName: String = ProcessInfo.processInfo.environment[LaunchKeys.viewController] else { return nil } var userInfo: [AnyHashable: Any]? = nil if let userInfoString: String = ProcessInfo.processInfo.environment[LaunchKeys.userInfo], let userInfoData: Data = userInfoString.data(using: .utf8) { userInfo = (try? JSONSerialization.jsonObject(with: userInfoData, options: [])) as? [AnyHashable : Any] } return creator.create(viewControllerName, userInfo: userInfo) } } 23
AppDelegate.swift let creator = Creator() let rootViewController: UIViewController #if DEBUG
if let viewController: UIViewController = Launcher(with: creator).launch() { rootViewController = viewController } else { rootViewController = creator.rootViewController() } #else rootViewController = creator.rootViewController() #endif window?.rootViewController = rootViewController 24
ͬͨࣄ ͦͷ2 25
UIςετ͔ΒViewControllerΛࢦఆͯ͠ ςετग़དྷΔ༷ʹͨ͠ 26
ςετଆͷλʔήοτʹ Launcher Λ࡞ 27
import XCTest struct Launcher { var viewControllerName: String var userInfo:
[AnyHashable: Any]? init(viewControllerName: String, userInfo: [AnyHashable: Any]? = nil) { self.viewControllerName = viewControllerName self.userInfo = userInfo } var env: [String: String] { var result: [String: String] = [LaunchKeys.viewController: self.viewControllerName] if let userInfo: [AnyHashable: Any] = self.userInfo { if let data: Data = try? JSONSerialization.data(withJSONObject: userInfo, options: []), let userInfoString: String = String(data: data, encoding: .utf8) { result[LaunchKeys.userInfo] = userInfoString } } return result } func launch() -> XCUIApplication { let app: XCUIApplication = XCUIApplication() app.launchEnvironment = env app.launch() return app } } 28
ͬͨࣄ ͦͷ3 29
ϖʔδͷཁૉΛཧ • σβΠφʔ͔ΒZeplinͱ͍͏πʔϧͰ UIΛ͏ • UIΛͬͨஈ֊ͰཁૉΛચ͍ग़͠ • ςετλʔήοτͰཁૉϦετΛ࡞ ʢPage Object
Design Patternʣ 30
PageObjectsRepresentable protocol protocol PageObjectsRepresentable { var app: XCUIApplication init(app: XCUIApplication)
} 31
Adopt to PageObjectsRepresentable struct ViewControllerPage: PageObjectsRepresentable { var app: XCUIApplication
init(app: XCUIApplication) { self.app = app } var label: XCUIElement { return app.staticTexts["label"] } } 32
Test import XCTest class LunchViewControllerTests: XCTestCase { var viewControllerName: String
{ return "ViewController" } func testLunchLabel() { let launcher = Launcher(viewControllerName: viewControllerName) let app = launcher.launch() let page = ViewControllerPage(app: app) XCTAssertTrue(page.label.exists) XCTAssertEqual(page.label.label, "Lunch") } } 33
͋Εʁ͜ΕϥΠϒϥϦԽ ग़དྷΔΜ͡Όͳ͍ʁ 34
https://github.com/ fromkk/Lunch 35
Lunch • Launcher • Creatable • LaunchKeys • PageObjectsRepresentable •
ViewControllerTestable ͷΈఆٛ 36
! ͍ͩ͘͞ 37
Ԡ༻ฤ 38
UIςετͰϞοΫͷΛөͤͯ͞ςετ 39
దͳϞσϧΛఆٛ struct HogeModel { var hoge: String } 40
ViewControllerʹϞσϧΛอ࣋ class HogeViewController: UIViewController { @IBOutlet weak var hogeLabel: UILabel!
var hogeModel: HogeModel { didSet { hogeLabel.text = hogeModel.hoge } } } 41
CreatorΛ֦ு struct Creator: Creatable { func create<T>(_ identifier: String, userInfo:
[AnyHashable : Any]?) -> T? { switch identifier { case "HogeViewController": guard let data = (userInfo?["MOCK_JSON"] as? String)?.data(using: .utf8), let json: [String: String] = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: String], let hoge = json["hoge"] else { return nil } let model = HogeModel(hoge: hoge) return hogeViewController(with: model) as? T default: return nil } } } extension Creator { func hogeViewController(with hoge: HogeModel) -> HogeViewController { let viewController = HogeViewController() viewController.hogeModel = hoge return viewController } } 42
UIςετ͔ΒJSONจࣈྻΛΞϓϦʹͯ͠ςετ func testModel() { let launcher = Launcher(viewControllerName: viewControllerName, userInfo:
["MOCK_JSON": "{\"hoge\": \"fuga\"}"]) let app = launcher.launch() let page = HogeViewControllerPage(app: app) XCTAssertTrue(page.hogeLabel.exists) XCTAssertEqual("fuga", page.hogeLabel.label) } 43
! 44
ͦͷଞͷςΟοϓε 45
UIͷ ঢ়ଶ Λςετ͢Δ • ཁૉ Λઃఆ͢Δͷ accessibilityIdentifier • ঢ়ଶ Λઃఆ͢Δͷ
accessibilityValue • छྨ Λઃఆ͢Δͷ accessibilityTraits • UIςετଆͰͦΕͧΕ identifier , value, elementType ͰΞΫηεग़དྷΔ 46
CIͰUIςετ࣮ߦ͠ͳ͍༷ʹ • CIڥ(Bitrise)ͰUIςετΛ࣮ߦ͢Δͱֻ͕͔࣌ؒΓ͗͢ Δ • xcodebuild test ʹ -only-testing:$UITEST_SCHEME ΛՃ͢ΔࣄͰςετ͢ΔεΩʔϜΛબ
• ಉ༷ʹ -skip-testing:$UITEST_SCHEME Ͱಛఆͷςε τͷεΩʔϜΛεΩοϓ͢Δࣄ͕ग़དྷΔ 47
ϩʔΧϥΠζରԠ • ϩέʔϧใΛมߋ͢Δʹ XCUIApplication ͷ launchArguments Λมߋ • ྫ: ["-AppleLanguages",
"(ja)", "- AppleLocale", "ja-JP"] Lunch ͷ߹ launcher(targetViewController: self, locale: "ja-JP") ͷ༷ʹࢦఆՄೳ 48
UIςετΛಋೖͯ͠Έͯ • Ұؾʹશ෦Λςετ͢ΔࣄΓ͍͠ • ग़དྷΔࣄ͔Βগͣͭ͠ " • ͕͜͜ಈ࡞͠ͳ͍ͱΞϓϦͱͯ͠Γཱͨͳ͍ͱ͍͏࠷ݶͷ ॴ͔ΒςετΛॻ͍ͯߦ͘ͷ͕ྑͦ͞͏ •
ϢχοτςετͰςετग़དྷΔՕॴͳΔ͘Ϣχοτςε τΛॻ͍ͨํ͕͍͍͔ $ 49
Summary • LunchͰUIςετͷϋʔυϧ͕গ͠Լ͕Γ·ͨ͠ • UIςετͰཁૉΛڞ௨Խ͢ΔࣄͰ͍ճ͕͠؆୯ • ಛఆͷViewController͚ͩ։͖͍ͨ࣌ʹεΩʔϜฤू͚ͩͰ ָνϯ # •
෭࣍తʹΠϯελϯεԽ͢ΔॲཧΛڞ௨Խग़དྷͨ 50
! ͍ͩ͘͞ https://github.com/fromkk/Lunch 51
͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠ 52