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.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
3
1.8k
1年分のデータが見たいと言われてやったこと/yearly_data_with_note
fromkk
0
920
note iOSチームの自動化 ver.2021/automation_with_iOS_team_on_note_ver2021
fromkk
0
1.9k
Bitrise体験会説明資料/bitrise_explore
fromkk
1
1k
noteのiOSアプリで実装したアクセシビリティの全て #iosdc #a /a11y_with_iOS_App_on_note
fromkk
2
3.6k
dSYMのアップロードで SPMを活用する/use_spm_with_upload_dsyms
fromkk
1
2.7k
Bitriseのリモートアクセス機能 #bitrise_meetup/remote_access_of_bitrise
fromkk
1
550
note社でのMagic Pod活用事例 #af_iosdc/magicpod_with_note
fromkk
2
11k
iOSには無いmacOS独自機能をCatalystで実装する #iosdc #d/make_macos_apps_with_catalyst
fromkk
9
2.1k
Other Decks in Programming
See All in Programming
Flutter × Firebase Genkit で加速する生成 AI アプリ開発
coborinai
0
170
たのしいSocketのしくみ / Socket Under a Microscope
coe401_
6
1k
color-scheme: light dark; を完全に理解する
uhyo
7
490
ソフトウェアエンジニアの成長
masuda220
PRO
12
2.1k
Open source software: how to live long and go far
gaelvaroquaux
0
660
CDK開発におけるコーディング規約の運用
yamanashi_ren01
2
240
5分で理解する SOLID 原則 #phpcon_nagoya
shogogg
1
290
責務と認知負荷を整える! 抽象レベルを意識した関心の分離
yahiru
8
1.3k
「個人開発マネタイズ大全」が教えてくれたこと
bani24884
1
130
GoとPHPのインターフェイスの違い
shimabox
2
210
React 19アップデートのために必要なこと
uhyo
7
1.4k
SwiftUI Viewの責務分離
elmetal
PRO
2
260
Featured
See All Featured
How to Ace a Technical Interview
jacobian
276
23k
[RailsConf 2023 Opening Keynote] The Magic of Rails
eileencodes
28
9.3k
jQuery: Nuts, Bolts and Bling
dougneiner
63
7.7k
Become a Pro
speakerdeck
PRO
26
5.2k
Responsive Adventures: Dirty Tricks From The Dark Corners of Front-End
smashingmag
251
21k
Fight the Zombie Pattern Library - RWD Summit 2016
marcelosomers
233
17k
Designing for Performance
lara
604
68k
Refactoring Trust on Your Teams (GOTO; Chicago 2020)
rmw
33
2.8k
"I'm Feeling Lucky" - Building Great Search Experiences for Today's Users (#IAC19)
danielanewman
226
22k
10 Git Anti Patterns You Should be Aware of
lemiorhan
PRO
656
59k
It's Worth the Effort
3n
184
28k
Optimising Largest Contentful Paint
csswizardry
34
3.1k
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