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_...
Search
fromkk
October 03, 2017
Programming
2
1.4k
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
3
1.9k
1年分のデータが見たいと言われてやったこと/yearly_data_with_note
fromkk
0
980
note iOSチームの自動化 ver.2021/automation_with_iOS_team_on_note_ver2021
fromkk
0
2k
Bitrise体験会説明資料/bitrise_explore
fromkk
1
1.1k
noteのiOSアプリで実装したアクセシビリティの全て #iosdc #a /a11y_with_iOS_App_on_note
fromkk
2
4.1k
dSYMのアップロードで SPMを活用する/use_spm_with_upload_dsyms
fromkk
1
3k
Bitriseのリモートアクセス機能 #bitrise_meetup/remote_access_of_bitrise
fromkk
1
610
note社でのMagic Pod活用事例 #af_iosdc/magicpod_with_note
fromkk
2
11k
iOSには無いmacOS独自機能をCatalystで実装する #iosdc #d/make_macos_apps_with_catalyst
fromkk
9
2.2k
Other Decks in Programming
See All in Programming
スマホから Youtube Shortsを見られないようにする
lemolatoon
27
34k
SwiftDataを使って10万件のデータを読み書きする
akidon0000
0
240
Devoxx BE - Local Development in the AI Era
kdubois
0
140
The Past, Present, and Future of Enterprise Java
ivargrimstad
0
280
TransformerからMCPまで(現代AIを理解するための羅針盤)
mickey_kubo
7
5.4k
bootcamp2025_バックエンド研修_WebAPIサーバ作成.pdf
geniee_inc
0
130
AkarengaLT vol.38
hashimoto_kei
1
120
20251016_Rails News ~Rails 8.1の足音を聴く~
morimorihoge
3
840
The Past, Present, and Future of Enterprise Java
ivargrimstad
0
500
コードとあなたと私の距離 / The Distance Between Code, You, and I
hiro_y
0
190
Claude CodeによるAI駆動開発の実践 〜そこから見えてきたこれからのプログラミング〜
iriikeita
0
350
はじめてのDSPy - 言語モデルを『プロンプト』ではなく『プログラミング』するための仕組み
masahiro_nishimi
4
16k
Featured
See All Featured
Building a Scalable Design System with Sketch
lauravandoore
463
33k
Mobile First: as difficult as doing things right
swwweet
225
10k
Visualization
eitanlees
149
16k
Reflections from 52 weeks, 52 projects
jeffersonlam
354
21k
Fashionably flexible responsive web design (full day workshop)
malarkey
407
66k
Music & Morning Musume
bryan
46
6.9k
The Success of Rails: Ensuring Growth for the Next 100 Years
eileencodes
46
7.7k
Site-Speed That Sticks
csswizardry
13
930
GitHub's CSS Performance
jonrohan
1032
470k
Art, The Web, and Tiny UX
lynnandtonic
303
21k
It's Worth the Effort
3n
187
28k
StorybookのUI Testing Handbookを読んだ
zakiyama
31
6.2k
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