Tag: testing
- Page Object Model
Page Object Model
Run test on different devices
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// File: TestCaseExtensions > BaseTestCase.swift import XCTest class BaseTestCase: XCTestCase { override func setUp() { super.setUp() continueAfterFailure = false } override func tearDown() { super.tearDown() } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// File: Classes > BaseConfiguration > BaseConfiguration.swift import Foundation import XCTest class BaseConfiguration { let app: XCUIApplication let testCase: BaseTestCase init(testCase: BaseTestCase) { app = testCase.app self.testCase = testCase } }
1 2 3 4 5 6 7 8 9 10 11
// File: Classes > AppConfiguration > AppConfiguration.swift import Foundation import XCTest class AppConfiguration: BaseConfiguration { override init(testCase: SKKTestCase) { super.init(testCase: testCase) } }
1 2 3 4 5 6 7 8 9 10 11
// File: TestCaseExtensions > BaseTestCase.swift import XCTest class BaseTestCase: XCTestCase { ... lazy var appConfiguration: AppConfiguration = { [unowned self] in return AppConfiguration(testCase: self) }() }
- Scrolling
Xcode
Scrolling
Scroll to an item
1 2 3 4 5
func isVisible(_ app: XCUIApplication) -> Bool { let window = app.windows.element(boundBy: 0).firstMatch return waitForElementToBecomeHittable(timeout: .small) && !frame(app).isEmpty && window.frame(app).contains(frame(app)) }
Source: Scroll helper
The helper function scrolls 1/2 the height of the collection View over and over until either the cell you are looking for is hittable, or the scroll doesn’t actually change anything (you hit the top or bottom of the collection view).
Note that the first check it does for the cell, looks like this:
1
collectionViewElement.cells.matchingIdentifier(cellIdentifier).count > 0
This lets you query the collectionView cells to see if the identifier is present without having the test fail by directly checking for the cell with collectionViewElement.cells[cellIdentifier], you would get a failure in the test, and it wouldn’t continue.
The code checks to see if the touch changed anything by keeping track of the ‘middle’ cell in the list of cells for the collection view (which by-the-way, might include cells that are no longer displayed), and determines if it’s the same cell id and same frame to see if anything has changed.
After the cell is found to be hittable, if you want it fully visible, there is a second loop that scrolls up or down by a smaller amount (1/2 the height of the cell) until the cell’s frame is fully contained by the collection view’s frame.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
import XCTest extension Page { @discardableResult func scrollTo( _ target: XCUIElement, scrollView collectionView: XCUIElement, scrollDirection: ScrollDirection = .bottomToTop, maxiumumAttempts: Int = 5, scrollDistance: CGFloat = 0.1 ) -> Bool { return smartScroll( target: target, scrollView: collectionView, scrollDirection: scrollDirection, maxiumumAttempts: maxiumumAttempts, scrollDistance: scrollDistance ) } private func smartScroll( target: XCUIElement, scrollView: XCUIElement, scrollDirection: ScrollDirection, maxiumumAttempts: Int, matchByLabel: String? = nil, scrollDistance: CGFloat ) -> Bool { /* * This currently works on the assumption that items are returned left to right and top to bottom. */ func midItem() -> XCUIElement? { let children = scrollView.children(matching: .any) return children.count > 0 ? children.element(boundBy: children.count / 2) : nil } var scrollAttempt = 0 var lastMidChildIdentifier = Optional("") var lastMidChildRect = Optional(CGRect.zero) var targetDistance: CGFloat = scrollDistance var currentMidChild = midItem() let elementIsNotInView = !(lastMidChildIdentifier == currentMidChild?.identifier && (currentMidChild?.frame(app) ?? CGRect.infinite).equalTo(lastMidChildRect ?? CGRect.zero)) // The default behaviour is to scroll the collection view until the element exits using the defined // scroll direction, but there are instances where it would make sense to reverse the scroll direction // if the element exists and the element is on the opposite side of the scroll direction. // An example of this is if we need the item to be centre with in the collection view but the item // is to the left of the centre item like in the a carousel. // // Example: // Scroll Direction: Right --> // // Current Item // v // | Item 5 | Item 1 | Item 2 | Item 3 | Item 4 | // ^ // Desired Item // // <-- Reverse The Scroll Direction func normalize(scrollDirection: ScrollDirection) -> ScrollDirection { guard target.waitForElementToBecomeHittable(timeout: .small) else { targetDistance = scrollDistance return scrollDirection } targetDistance = 0.5 let elementFrame = target.frame(app) let scollViewFrame = scrollView.frame(app) // Calculate the element position compared to the scrollView let elementIsToTheLeft = elementFrame.midX < scollViewFrame.midX let elementIsToTheBottom = elementFrame.midY > scollViewFrame.midY switch scrollDirection { case .leftToRight where !elementIsToTheLeft: return scrollDirection.inverted() case .rightToLeft where elementIsToTheLeft: return scrollDirection.inverted() case .topToBottom where elementIsToTheBottom: return scrollDirection.inverted() case .bottomToTop where !elementIsToTheBottom: return scrollDirection.inverted() default: return scrollDirection } } // Wait for ScrollView to exist _ = scrollView.waitForExistence(timeout: .small) while elementIsNotInView && scrollAttempt < maxiumumAttempts { let collectionViewValue = scrollView.value as? String ?? "" if target.isVisible(app) || matchByLabel != nil && collectionViewValue == matchByLabel { return true } lastMidChildIdentifier = currentMidChild?.identifier lastMidChildRect = currentMidChild?.frame(app) let (startOffset, endOffset) = normalize(scrollDirection: scrollDirection).vectors(targetDistance: targetDistance) scrollView .coordinate(withNormalizedOffset: startOffset) .press(forDuration: 0.01, thenDragTo: scrollView.coordinate(withNormalizedOffset: endOffset)) scrollAttempt += 1 currentMidChild = midItem() } return false } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
enum ScrollDirection { case topToBottom case bottomToTop case leftToRight case rightToLeft func inverted() -> ScrollDirection { switch self { case .topToBottom: return .bottomToTop case .bottomToTop: return .topToBottom case .leftToRight: return .rightToLeft case .rightToLeft: return .leftToRight } } func vectors(targetDistance: CGFloat) -> (start: CGVector, finish: CGVector) { switch self { case .topToBottom: return (start: CGVector(dx: 0.99, dy: targetDistance), finish: CGVector(dx: 0.99, dy: 0.9)) case .bottomToTop: return (start: CGVector(dx: 0.99, dy: 0.9), finish: CGVector(dx: 0.99, dy: targetDistance)) case .leftToRight: return (start: CGVector(dx: targetDistance, dy: 0.99), finish: CGVector(dx: 0.9, dy: 0.99)) case .rightToLeft: return (start: CGVector(dx: 0.9, dy: 0.99), finish: CGVector(dx: targetDistance, dy: 0.99)) } } }
- Testing on different devices
Testing on different devices
Run test on different devices
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
import Foundation import XCTest public extension XCUIApplication { /// Check if application is running on simulator public var isRunningOnSimulator: Bool { #if targetEnvironment(simulator) return true #else return false #endif } public var isRunningOnRealDevice: Bool { return !isRunningOnSimulator } public var isRunningOnTablet: Bool { return UIDevice.current.userInterfaceIdiom == .pad } public var isRunningOnPhone: Bool { return UIDevice.current.userInterfaceIdiom == .phone } }
1 2 3 4 5 6 7 8
// File: TestCaseExtensions > DeviceTypeProtocol.swift protocol TestOnAnyDevice {} protocol TestOnPhone {} protocol TestOnTablet {} protocol TestOnRealDevice {} protocol TestOnSimulator {}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
// File: TestCaseExtensions > BaseTestCase.swift import XCTest class BaseTestCase: XCTestCase { // Check if tests are compatible with the current device override func perform(_ run: XCTestRun) { let test = run.test let testOnPhone = test is TestOnPhone let testOnRealDevice = test is TestOnRealDevice let testOnSimulator = test is TestOnSimulator let testOnTablet = test is TestOnTablet if (testOnRealDevice && app.isRunningOnSimulator) || (testOnSimulator && app.isRunningOnRealDevice) { return reasonTestWasSkipped(test.name, message: "Not supported for the current hardware type") } if (testOnPhone && app.isRunningOnTablet) || (testOnTablet && app.isRunningOnPhone) { return reasonTestWasSkipped(test.name, message: "Not supported for the current device type") } super.perform(run) } private func reasonTestWasSkipped(_ testName: String, message: String) { let messageForSkipedTest = """ -- Test Skipped -- \(testName) [\(message)] """ return print(messageForSkipedTest) } }
1 2 3 4 5 6 7 8 9 10 11
// File: UITest > ExampleTest.swift import XCTest class ExampleTest: BaseTestCase, TestOnPhone { func testWaitForElement() { ... } }
1 2 3 4 5 6 7 8 9 10 11
// File: UITest > ExampleTestTablet.swift import XCTest class ExampleTestTwo: BaseTestCase, TestOnTablet, TestOnSimulator { func testWaitForElement() { ... } }
- Xcode setup
Xcode
Xcode setup for UI testing
Create UI testing target.
If you have an existing project and would like to add automated UI tests to it, first you need to create iOS UI testing target. This is how you do it.
- Open your Xcode project.
- Go to: File -> New -> Target
- From the window Choose a template for your new target: select iOS UI Testing Bundle and hit Next:
- From the window Choose options for your new target: select your Team and Target to be tested
- Select Finish button and new test target has been created.
Create UI test file.
- Pick the location in your Project navigator where would you like your test file to be created.
- Right-click and select New File…
- From the window Choose a template for your new file select UI Test Case Class and hit Next button.
- In the Choose options for your new file: window provide class name and hit Next button.
- Select the location where you want the file to be created and hit Create button.
- You have just created your UI test class with setUp, tearDown and testExample methods.
{% highlight ruby linenos %} def show … end {% endhighlight %}
Though you can add as many categories as you like, I recommend not to exceed 10. Too much of anything is bad.
- xcuitest