To make UI testing work it has to be automated, easy to maintain and fast. This talk is how to use KIF testing framework for automated UI testing for iOS apps using Swift.
2. CASE FOR UI TESTING
INTEGRATION
NAVIGATION FLOW
DATA FLOW
USER EXPERIENCE
ACCESSIBILITY
REFACTORINGS / REGRESSIONS
3. UI TESTING IS HARD
AND TIME CONSUMING
UNLESS
AUTOMATED
EASYTO MAINTAIN
FAST
4.
5. NATIVE SUPPORT
FOR UI TESTING
UI AUTOMATION (JavaScript)
XCUI (Swift/Objective C)
BASED ON UIAcessibility PROTOCOL
6. KIF
Keep 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
7. OBJECTIVE C - KIFTestCase
RUNS IN APP TARGET
beforeAll, beforeEach, afterAll, afterEach
KIFUITestActor - USER ACTIONS
KIFSystemTestActor - SYSTEM/DEVICE ACTIONS
SWIFT - XCTestCase
ADD SIMPLE EXTENSION TO ACCESS
KIFUITestActor AND KIFSystemTestActor
9. 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()
10. PROTOCOLTO 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
}
11. 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
}
12. 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
}
}
15. GOTCHAS & HINTS
OCCASIONALLY CAN FAIL WITHOUT A GOOD REASON
¯_( )_/¯
BEWARE OF UITableViewCells
ADD EVERYTHING AS SUBVIEWTO .contentView
SAVE SCREENSHOTS ON TEST FAILURE
SET ENVVARIABLE WITH FOLDER LOCATION- KIF_SCREENSHOTS= …
AFTER FAILING TEST IN MODAL VIEW ALL OTHER TEST RESULTS = USELESS
do { try tester().trySomething(); // then test } catch {}