USER INTERFACE
TESTING WITH KIF & SWIFT
CASE FOR UI TESTINGINTEGRATION
NAVIGATION FLOWDATA FLOW
USER EXPERIENCEACCESSIBILITY
REFACTORINGS / REGRESSIONS
UI TESTING IS HARD AND TIME CONSUMING
UNLESSAUTOMATED
EASY TO MAINTAIN FAST
NATIVE SUPPORT FOR UI TESTING
UI AUTOMATION (JavaScript)XCUI (Swift/Objective C)
BASED ON UIAcessibility PROTOCOL
KIFKeep It Functional - An iOS Functional
Testing Framework
https://github.com/kif-framework/KIF
UICONF 2016 TALK BY ELLEN SHAPIRO https://youtu.be/hYCUy-9yq_M
OBJECTIVE C - KIFTestCase RUNS IN APP TARGET
beforeAll, beforeEach, afterAll, afterEachKIFUITestActor - USER ACTIONS
KIFSystemTestActor - SYSTEM/DEVICE ACTIONS SWIFT - XCTestCase
ADD SIMPLE EXTENSION TO ACCESS KIFUITestActor AND KIFSystemTestActor
OBJECTIVE C [tester tapViewWithAccessibilityLabel:@"Login"];
SWIFT
tester().tapView(withAccessibilityLabel : "Login")
EXTENSIONS FOR READIBILITY extension KIFUITestActor {
@discardableResult func waitForButton(_ label: String) -> UIView! { let button = self.waitForTappableView(withAccessibilityLabel: label, traits: UIAccessibilityTraitButton)
return button }
func tapButton(_ accessibilityLabel: String, value: String) { let button = waitForButton(accessibilityLabel, value: value) button?.tap() }
}
RESULT
tester().tapButton(“Login”) // or even better – tester().tapLoginButton()
PROTOCOL TO ACCESS APP DATA public protocol UsesCoreDataDatabase { func dbContext() -> NSManagedObjectContext func isDbEmpty(_ context: NSManagedObjectContext) -> Bool func deleteDbData(_ context: NSManagedObjectContext) }
public extension UsesCoreDataDatabase { func dbContext() -> NSManagedObjectContext { let appDelegate: YourAppDelegate = UIApplication.shared.delegate as! YourAppDelegate let context = appDelegate.value(forKey: "context") as! NSManagedObjectContext XCTAssertNotNil(context) return context! }
func isDbEmpty(_ context: NSManagedObjectContext) -> Bool { // access model objects from context let count = …. return (count == 0) }
func deleteDbData(_ context: NSManagedObjectContext) { // Delete data }
CUSTOM TEST CASES class MyTestCaseWithEmptyDatabase: KIFTestCase, UsesCoreDataDatabase {
override func beforeEach() { let context = dbContext()
if ! sDbEmpty(context) { let name = MyAppServices.deviceModel() if name == "iPhone Simulator" { deleteDbData(context) } else { } // Are you sure want to delete data from device? }
} }
class MyTestCaseWithWizardFilled: MyTestCaseWithEmptyDatabase { var rentAmount = “100”; var unitName= “M10”; var tenantName = “Anna" override func beforeEach() { super.beforeEach() let context = dbContext() MyWizardController.saveFromWizard(in: context, address: unitName, tenant: tenantName amount: rentAmount) }
}
class MyTestCaseWithFixturesLoaded : MyTestCaseWithEmptyDatabase { // load fixture data in database
}
DRY, READABLE TESTS class PaymentTests: MyTestCaseWithWizardFilled { func testAddPayment_fromUnitDashboard() {
// given - tenantName, unitName, today are defined as MyTestCaseWithWizardFilled class variables let amount = “5.1”; let paymentAmount = “$5.10”
tester().tapPropertiesTabButton() tester().tapUnit(unitName, tenant: tenantName) tester().waitForTenantBalanceScreen(tenantName, currentBalance: "$0.00") tester().waitForLastPaymentLine("No rent payments received", amount: “") // even this should be replaced by waitForNoLastPaymentLine // when tester().tapAddPaymentButton() tester().enterOnKeyboard(amount) tester().tapSaveButton() // then tester().waitForTenantBalanceScreen(tenantName, currentBalance: paymentAmount) tester().waitForLastRentPaymentLine(today, amount: paymentAmount)
} } … public extension KIFUITestActor { /// Returns last payment line in rental unit dashboard @discardableResult func waitForLastPaymentLine(_ date: String, amount: String) -> UIView! { let view = waitForView("Last rent payment", value: "\(date), \(amount)”) // Accessibility label & accessibility value return view } }
FAST
NOT REALLY
MAKE UI TESTS FASTERclass MyTestCaseWithEmptyDatabase: KIFTestCase, UsesCoreDataDatabase { override func beforeAll() { UIApplication.shared.keyWindow!.layer.speed = 100; // faster animations } }
GOTCHAS & HINTSOCCASIONALLY CAN FAIL WITHOUT A GOOD REASON
¯\_( )_/¯
BEWARE OF UITableViewCellsADD EVERYTHING AS SUBVIEW TO .contentView
SAVE SCREENSHOTS ON TEST FAILURESET ENV VARIABLE WITH FOLDER LOCATION- KIF_SCREENSHOTS= …
AFTER FAILING TEST IN MODAL VIEW ALL OTHER TEST RESULTS = USELESS
do { try tester().trySomething(); // then test } catch {}
ACCESSIBILITYhttps://youtu.be/no12EfZUSQo
“You have no clue about your app accessibility until you write app UI tests
that rely on accessibility” / me to myself /
UIAccessibility PROTOCOLUIAccessibilityElement CLASS
UIAccessibilityContainer PROTOCOL UIAccessibilityTraits
BUNDLED FREE WHEN USING UIKit, JUST SET
accessibilityLabel, accessibilityValue, accessibilityHint IN IB AND/OR CODE
QUESTIONS?
Jurģis Ķiršakmens [email protected] @jki / @xjki