Slide 1

Slide 1 text

πʔϧͱͯ͠ར༻͢ΔUIςετ iOSDC 2018 લ໷ࡇ Track A !1

Slide 2

Slide 2 text

Profile struct Profile { let name = "Kazuya Ueoka" let twitter = "@fromkk" let github = "fromkk" let qiita = "fromkk" let company = "Timers Inc." } • !2

Slide 3

Slide 3 text

͜Μͳࣄແ͍Ͱ͔͢ʁ !3

Slide 4

Slide 4 text

UIͷ࣮૷͕׬ྃ ͱࢥ͍͖΍ !4

Slide 5

Slide 5 text

༷ʑͳ୺຤ͰͷσβΠϯ่Ε •ҟͳΔը໘αΠζ •iPhone͚ͩ͡Όͳ͘iPadͷରԠ ଞʹ΋ •ԣɾॎͷ੾Γସ͑ •ҟͳΔOSόʔδϣϯ •ӳޠ͸จষ͕௕͘ͳΓ͕ͪ໰୊ !5

Slide 6

Slide 6 text

ϨΠΞ΢τ่ΕΛͳΔ΂͘ૣ͍ஈ֊Ͱݕ஌͍ͨ͠ # Ϣχοτςετ UIςετ ਓͷͰ໨ࢹ !6

Slide 7

Slide 7 text

% UIςετ➕ਓͷͰ໨ࢹ ը໘αΠζ΍ݴޠຖͷεΫγϣΛ 1ຕͷը૾ʹग़དྷΕ͹ྑͦ͞͏ !7

Slide 8

Slide 8 text

UIςετͷৼΓฦΓ !8 https://developer.apple.com/videos/play/wwdc2015/406/

Slide 9

Slide 9 text

UIςετΛ͍ͯ͠Δͱͨ͘͠ͳΔࣄ • ಛఆͷը໘ʹ௚઀ભҠ • ݴޠ΍஍ҬΛมߋ • ϞοΫσʔλΛ౉ͯ͠ঢ়ଶมԽ • etc…

Slide 10

Slide 10 text

https://github.com/fromkk/Lunch Lunch https://speakerdeck.com/fromkk/orecon-ios-ui-test-20171003 !10 2017

Slide 11

Slide 11 text

LunchͷΠϯετʔϧ • Carthageܦ༝ͰΠϯετʔϧՄೳ • Cartfileʹgithub "fromkk/Lunch"Λ௥Ճ • carthage updateΛ࣮ߦ • ΞϓϦɾUIςετͦΕͧΕͷBuild Phraseʹ/usr/local/bin/carthage copy-frameworksΛ௥Ճ • ΞϓϦλʔήοτͷInput Filesʹ$(SRCROOT)/Carthage/Build/iOS/ Lunch.frameworkΛ௥Ճ • UIςετλʔήοτͷInput Filesʹ$(SRCROOT)/Carthage/Build/iOS/ LunchTest.frameworkΛ௥Ճ !11

Slide 12

Slide 12 text

Lunchͷ࢖͍ํ(ΞϓϦଆ) import Lunch class Creator: Creatable { func create(_ identifier: String, userInfo: [AnyHashable: Any]?) -> T? { guard let name = ViewControllerName(rawValue: identifier) else { return nil } switch name { case .rootViewController: return RootViewController() as? T case .registerViewController: return RegisterViewController() as? T case .helpViewController: return HelpViewController() as? T case .contactViewController: return ContactViewController() as? T } } !12

Slide 13

Slide 13 text

Lunchͷ࢖͍ํ(ΞϓϦଆ) import UIKit import Lunch func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) window?.backgroundColor = .white let rootViewController: UIViewController let creator: Creator = Creator() #if DEBUG if let viewController: UIViewController = Launcher(with: creator).launch() { rootViewController = viewController } else { rootViewController = creator.rootViewController() } #else rootViewController = creator.rootViewController() #endif window?.rootViewController = rootViewController window?.makeKeyAndVisible() } !13

Slide 14

Slide 14 text

Lunchͷ࢖͍ํ(ΞϓϦଆ) import UIKit import Lunch func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) window?.backgroundColor = .white let rootViewController: UIViewController let creator: Creator = Creator() #if DEBUG if let viewController: UIViewController = Launcher(with: creator).launch() { rootViewController = viewController } else { rootViewController = creator.rootViewController() } #else rootViewController = creator.rootViewController() #endif window?.rootViewController = rootViewController window?.makeKeyAndVisible() } !14

Slide 15

Slide 15 text

Lunchͷ࢖͍ํ(ΞϓϦଆ) import UIKit import Lunch func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) window?.backgroundColor = .white let rootViewController: UIViewController let creator: Creator = Creator() #if DEBUG if let viewController: UIViewController = Launcher(with: creator).launch() { rootViewController = viewController } else { rootViewController = creator.rootViewController() } #else rootViewController = creator.rootViewController() #endif window?.rootViewController = rootViewController window?.makeKeyAndVisible() } !15

Slide 16

Slide 16 text

Lunchͷ࢖͍ํ(ςετଆ) import XCTest import LunchTest final class RootViewControllerTest: XCTestCase, ViewControllerTestable { var viewControllerName: String { return ViewControllerName.rootViewController.rawValue } func launch(with locale: String) -> XCUIApplication { let launcher = Launcher(targetViewController: self, locale: locale) return launcher.launch() } func testInitializeJP() { let _ = launch(with: "ja_JP") // TODO: something todo } } !16

Slide 17

Slide 17 text

Lunchͷ࢖͍ํ(ςετଆ) import XCTest import LunchTest final class RootViewControllerTest: XCTestCase, ViewControllerTestable { var viewControllerName: String { return ViewControllerName.rootViewController.rawValue } func launch(with locale: String) -> XCUIApplication { let launcher = Launcher(targetViewController: self, locale: locale) return launcher.launch() } func testInitializeJP() { let _ = launch(with: "ja_JP") // TODO: something todo } } !17

Slide 18

Slide 18 text

Lunchͷ࢖͍ํ(ςετଆ) import XCTest import LunchTest final class RootViewControllerTest: XCTestCase, ViewControllerTestable { var viewControllerName: String { return ViewControllerName.rootViewController.rawValue } func launch(with locale: String) -> XCUIApplication { let launcher = Launcher(targetViewController: self, locale: locale) return launcher.launch() } func testInitializeJP() { let _ = launch(with: "ja_JP") // TODO: something todo } } !18

Slide 19

Slide 19 text

Lunchͷ࢖͍ํ(ςετଆ) import XCTest import LunchTest final class RootViewControllerTest: XCTestCase, ViewControllerTestable { var viewControllerName: String { return ViewControllerName.rootViewController.rawValue } func launch(with locale: String) -> XCUIApplication { let launcher = Launcher(targetViewController: self, locale: locale) return launcher.launch() } func testInitializeJP() { let _ = launch(with: "ja_JP") // TODO: something todo } } !19

Slide 20

Slide 20 text

͍ͩ͘͞ https://github.com/fromkk/Lunch !20

Slide 21

Slide 21 text

Xcode 9ΑΓXCTestʹScreenshots! https://developer.apple.com/videos/play/wwdc2017/409/ !21

Slide 22

Slide 22 text

εΫγϣΛࡱΔϝιουΛੜ΍͢ import XCTest extension XCTestCase { func screenshot(_ named: String) { XCTContext.runActivity(named: named, block: { activity in let screenshot = XCUIScreen.main.screenshot() let attachment = XCTAttachment(screenshot: screenshot) attachment.lifetime = .keepAlways activity.add(attachment) }) } } !22

Slide 23

Slide 23 text

εΫγϣΛࡱΔϝιουΛੜ΍͢ import XCTest extension XCTestCase { func screenshot(_ named: String) { XCTContext.runActivity(named: named, block: { activity in let screenshot = XCUIScreen.main.screenshot() let attachment = XCTAttachment(screenshot: screenshot) attachment.lifetime = .keepAlways activity.add(attachment) }) } } !23

Slide 24

Slide 24 text

εΫγϣΛࡱΔϝιουΛੜ΍͢ import XCTest extension XCTestCase { func screenshot(_ named: String) { XCTContext.runActivity(named: named, block: { activity in let screenshot = XCUIScreen.main.screenshot() let attachment = XCTAttachment(screenshot: screenshot) attachment.lifetime = .keepAlways activity.add(attachment) }) } } !24

Slide 25

Slide 25 text

εΫγϣΛࡱΔϝιουΛੜ΍͢ import XCTest extension XCTestCase { func screenshot(_ named: String) { XCTContext.runActivity(named: named, block: { activity in let screenshot = XCUIScreen.main.screenshot() let attachment = XCTAttachment(screenshot: screenshot) attachment.lifetime = .keepAlways activity.add(attachment) }) } } !25

Slide 26

Slide 26 text

৭Μͳը໘Λ։͍ͯͻͨ͢ΒεΫγϣ func testInitializeJP() { let app = launchOtherView(with: "ja_JP") screenshot(#function + " screenshot") } func testInitializeUS() { let app = launchOtherView(with: "en_US") screenshot(#function + " screenshot") } . . . !26

Slide 27

Slide 27 text

UIςετͷ࣮ߦ • xcodebuild build-for-testing ϏϧυͷΈ࣮ߦ • xcodebuild test-without-building ςετͷΈ࣮ߦ • Φϓγϣϯ • -project .xcodeprojϑΝΠϧ • -workspace .xcworkspaceϑΝΠϧ • -scheme Ϗϧυ͢ΔεΩʔϚ • -configuration Debug΍ReleaseͳͲ • -sdk iphonesimulator΍iphoneosͳͲ • -destination “name=iPhone X,OS=11.4,platform=iOS Simulator” • -only-testing:HogeTests -skip-testing:FugaTests !27

Slide 28

Slide 28 text

ShellscriptԽ͓ͯ͘͠ͱศར #!/bin/sh xcodeproj=./Type.xcodeproj xcworkspace=./Type.xcworkspace isWorkspace=true test="-only-testing:TypeUITests" scheme="Type" configuration="Debug" sdk="iphonesimulator" devices=("iPhone SE" "iPhone 7" "iPhone 7 Plus" "iPhone X") osList=("10.3.1" "11.4") if $isWorkspace; then command="xcodebuild -workspace $xcworkspace" else command="xcodebuild -project $xcodeproj" fi rm -rf ./result xcrun simctl shutdown xcrun simctl erase all open -a Simulator.app command="$command -scheme $scheme -sdk $sdk -configuration $configuration" $command clean $command build-for-testing for ((i = 0; i < ${#devices[@]}; i++)); do device=${devices[$i]} for os in ${osList[@]}; do destination="platform=iOS Simulator,name=${device},OS=${os}" result=./result/"${device}"_"${os}".bundle echo $result $command test-without-building -destination "$destination" $test -resultBundlePath "$result" done done https://gist.github.com/fromkk/95b7fd046a081da5bfac913e1c7b31a8 !28

Slide 29

Slide 29 text

ୠ͠ɺ͜ͷ··ͩͱ஗͍ ෳ਺୺຤ɾOSΛ௚ྻͰ࣮ߦ͍ͯ͠Δ !29

Slide 30

Slide 30 text

!30 https://developer.apple.com/videos/play/wwdc2018/403/

Slide 31

Slide 31 text

!31

Slide 32

Slide 32 text

!32

Slide 33

Slide 33 text

!33

Slide 34

Slide 34 text

!34

Slide 35

Slide 35 text

!35 1ճ໨ 2ճ໨ 3ճ໨ 4ճ໨ 5ճ໨ ฏۉ 1 Simulator 145.68s 150.7s 146.76s 146.76s 153.15s 148.61s 2 Simulators 118.4s 125.44s 115.96s 115.57s 112.9s 117.65s 3 Simulators 124.72s 125.44s 115.09s 126.14s 111.81s 120.64s 4 Simulators 124.53s 134.79s 132.47s 144.06s 126.19s 132.41s # TypeͰUIςετ૸ΒͤͯΈͨ݁Ռ

Slide 36

Slide 36 text

ςετ݁ՌΛը૾ʹ·ͱΊ͍ͨ • xcodebuildίϚϯυʹ-resultBundlePathΦϓγϣϯΛ౉͢ !36

Slide 37

Slide 37 text

!37 TestSummaries.plistΛύʔεͯ͠ը૾Λ͔͖ूΊΔ

Slide 38

Slide 38 text

https://github.com/fromkk/TestSummaries • TestSummaries.plistΛύʔεͯ͠Attachmentsͷը૾ͱϚοϐϯά • HTMLॻ͖ग़͠ͱPNGॻ͖ग़͠ػೳ͕͋Δ XCTestͷUIςετͷ݁ՌΛΠΠײ͡Ͱݟ͍ͨ https://qiita.com/fromkk/items/38d8d4139053ae33773f TestSummaries !38 New

Slide 39

Slide 39 text

TestSummariesͷΠϯετʔϧ brew install fromkk/TestSummaries/testsummaries !39

Slide 40

Slide 40 text

TestSummariesͷ࢖͍ํ test-summaries [--resultDirectory ] | [--bundlePath ] --outputPath --outputType [--imageScale ] [--backgroundColor ] [--textColor ] !40

Slide 41

Slide 41 text

TestSummariesͷ࣮ߦ # ςετͷ࣮ߦ $ xcodebuild test -workspace Type.xcworkspace -scheme Type - configuration Debug -sdk iphonesimulator -destination “OS=11.4,name=iPhone X,platform=iOS Simulator" -resultBundlePath ./ uitests/iphonex.bundle . . . # ݁ՌΛ·ͱΊΔ $ test-summaries --resultDirectory ./uitests --outputPath ./ uitests/result.png —outputType PNG --imageScale 2 !41

Slide 42

Slide 42 text

TestSummariesͷ࣮ߦ # ςετͷ࣮ߦ $ xcodebuild test -workspace Type.xcworkspace -scheme Type - configuration Debug -sdk iphonesimulator -destination “OS=11.4,name=iPhone X,platform=iOS Simulator" -resultBundlePath ./ uitests/iphonex.bundle . . . # ݁ՌΛ·ͱΊΔ $ test-summaries --resultDirectory ./uitests --outputPath ./ uitests/result.png —outputType PNG --imageScale 2 !42

Slide 43

Slide 43 text

݁Ռ !43 ݁Ռ

Slide 44

Slide 44 text

!44

Slide 45

Slide 45 text

͍ͩ͘͞ https://github.com/fromkk/TestSummaries !45

Slide 46

Slide 46 text

Demo !46

Slide 47

Slide 47 text

͜͜·ͰͷৼΓฦΓ • XCTAssertແ͠ͰUIςετΛ࣮ߦ • εΫϦʔϯγϣοτΛࡱӨ • εΫϦʔϯγϣοτը૾Λ1ຕʹ·ͱΊΔ !47

Slide 48

Slide 48 text

͜ͷঢ়ଶ͔ΒͳΒXCTAssertΛ গͣͭ͠௥Ճग़དྷͦ͏͡Όͳ͍ʁ !48

Slide 49

Slide 49 text

ྫ͑͹ func testHoge() { let app = launchOtherView(with: "ja_JP") screenshot(#function + " screenshot") } !49

Slide 50

Slide 50 text

ྫ͑͹ func testHoge() { let app = launchOtherView(with: “ja_JP") let hogeLagel = app.staticTexts["hogeLagel"] XCTAssertTrue(hogeLagel.exists) XCTAssertEqual(hogeLagel.label, "hoge") screenshot(#function + " screenshot") } !50

Slide 51

Slide 51 text

ྫ͑͹ func testHoge() { let launcher = Launcher(targetViewController: self) let app = launcher.launch() XCTAssertTrue(app.staticTexts[“hogeLabel"].exists) XCTAssertEqual(app.staticTexts["hogeLabel"].label, "hoge") screenshot(#function) } !51

Slide 52

Slide 52 text

ྫ͑͹ func testHoge() { let launcher = Launcher(targetViewController: self, userInfo: [ "MOCK_JSON": "{\"hoge\": \"fuga\"}" ]) let app = launcher.launch() XCTAssertTrue(app.staticTexts["hogeLabel"].exists) XCTAssertEqual(app.staticTexts["hogeLabel"].label, "fuga") screenshot(#function) } !52

Slide 53

Slide 53 text

·ͱΊ • UIςετΛςετͱͯ͠ͷ༻్Ͱ͸ͳࣗ͘ಈεΫγϣࡱӨػೳͱͯ͠ ར༻ͯ͠Έͨ • ෳ਺ͷεΫϦʔϯγϣοτΛ̍ຕͷը૾ʹ·ͱΊΔࣄͰɺσβΠϯ่ ΕΛ໨ࢹͰ֬ೝग़དྷΔ༷ʹͯ͠Έ·ͨ͠ • ੜ੒͞ΕΔͷ͕ը૾ͳͷͰνʔϜ΁ͷڞ༗΋؆୯* • ඞཁͳը໘͕શͯىಈग़དྷΔ༷ʹͳͬͨΒςετ΋ॻ͚Δؾ͕͠·ͤ Μ͔ʁ !53

Slide 54

Slide 54 text

͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠+ !54