striking a balance with ui tests - connecttech
TRANSCRIPT
Striking a Balance with UI TestsBy: Jesse BlackSoftware Engineerstable|kernel [email protected]
@stablekernel
Hi, I’m Jesse Black.
• Programming for over nine years• Created a Mac App for my family business• Worked for 3 years with Gramercy Consultants developing iOS and Android apps• Working for stable|kernel for the past 4 years developing iOS apps, Android apps
and their supporting APIs
We’re stable|kernel. stable|kernel is an Atlanta-based mobile development company to craft smartly-designed mobile applications that connect brands directly with their users.
Striking a Balance with UI Tests
@stablekernel
Overview
• General Testing• Demo: Introducing UI Tests• Improving UI Tests with Robots• Mocking Dependencies• Discussion
Types of Tests
@stablekernel
• Manual testing• Automated testing• Unit / Integrated / End to End
• Continuous Testing
Unit Tests
@stablekernel
• Tests a small amount of code with limited inputs and output• Should be F.I.R.S.T.
• Fast• Isolated• Repeatable• Self - Verifying• Timely
Why test?
@stablekernel
• Fast feedback• Help prevent regressions• Improve code design
Demo
@stablekernel
UI Test Recording and Playback
Recording observations
@stablekernel
• Black box• User’s perspective• No Asserts (Assertion by tapping)
Example: Login Error
@stablekernel
let app = XCUIApplication()let usernameTextField = app.textFields["Username"]usernameTextField.tap()usernameTextField.typeText("invalid")app.children(matching: .window).element(boundBy: 0).children(matching: .other).element.children(matching: .other).element(boundBy: 0).tap() let passwordSecureTextField = app.secureTextFields["Password"]passwordSecureTextField.tap()passwordSecureTextField.typeText("invalid")app.children(matching: .window).element(boundBy: 0).children(matching: .other).element.children(matching: .other).element(boundBy: 0).tap()app.buttons[“Login"].tap() app.alerts["Username and password combo not found."].buttons["OK"].tap()
Example: Login Error
@stablekernel
let app = XCUIApplication()let usernameTextField = app.textFields["Username"]usernameTextField.tap()usernameTextField.typeText("invalid")app.children(matching: .window).element(boundBy: 0).children(matching: .other).element.children(matching: .other).element(boundBy: 0).tap() let passwordSecureTextField = app.secureTextFields["Password"]passwordSecureTextField.tap()passwordSecureTextField.typeText("invalid")app.children(matching: .window).element(boundBy: 0).children(matching: .other).element.children(matching: .other).element(boundBy: 0).tap()app.buttons[“Login"].tap() app.alerts["Username and password combo not found."].buttons["OK"].tap()
• Scary strings• Will fail without network• Chunks of very similar code• Fragile tap to dismiss lines
Wrangle user facing strings
@stablekernel
• Localize all user facing strings• Add localized string file to both targets; app and UI Test targets• Create abstraction around NSLocalizedString to avoid using stringly
typed keys when accessing user facing strings
Wrangle user facing string
@stablekernel
enum UIString: String { case usernamePlaceholder = "username-placeholder"
func localized() -> String { return UIStringsHelper.getLocalizedString(key: rawValue) }}
private class UIStringsHelper { static func getLocalizedString(key: String) -> String { let bundle = Bundle(for: UIStringsHelper.self) return NSLocalizedString(key, bundle: bundle, comment: "") }}
Factor out user actions into Robots
@stablekernel
struct LoginRobot { let app: XCUIApplication
var usernameTextField: XCUIElement { return app.textFields[UIString.usernamePlaceholder.localized()] }
func dismissKeyboard() { app.children(matching: .window).element(boundBy: 0).children(matching: .other).element.children(matching: .other).element(boundBy: 0).tap() }
func enterUsername(text: String) { usernameTextField.tap() usernameTextField.typeText(text) }
}
Example: Login Error Refactored
@stablekernel
func testInvalidCredentials() {loginRobot.enterUsername(text: "invalid@email")loginRobot.dismissKeyboard()
loginRobot.enterPassword(text: "invalid")loginRobot.dismissKeyboard()
loginRobot.loginButton.tap()
let alertRobot = AlertRobot(app: XCUIApplication(), title: "Username and password combo not found.")alertRobot.dismiss(withButtonTitled: UIString.okButton.localized())
}
Dependencies
@stablekernel
• Data persistence• Networks• Third Party Libraries
Mock Out Dependencies
@stablekernel
Mock Example: Login Credentials
@stablekernel
// UI Test Setupoverride func setUp() { super.setUp()
// ...
XCUIApplication().launchArguments = [“USE_MOCK_LOGIN_STORE”, “HAS_SAVED_USERNAME”] XCUIApplication().launch()}
Mock Example: Login Credentials
@stablekernel
protocol LoginCredentialsStore: class { func saveUsername(username: String?) func readUsername() -> String?}
Mock Example: Login Credentials
@stablekernel
class MockLoginCredentialsStore: LoginCredentialsStore {
var savedUsername: String?
init() { if CommandLine.arguments.contains("HAS_SAVED_USERNAME") { savedUsername = "[email protected]" } }
func saveUsername(username: String?) { savedUsername = username }
func readUsername() -> String? { return savedUsername }}
Realistic Goals
@stablekernel
• Automate screen shots of your app as you roll out changes• Be prepared for demos with stakeholders as you close out units of work
• Increase confident that previously demoed work does not have any regressions
Final Remarks
@stablekernel
Questions?
Business Inquiries:Sarah WoodwardDirector of Business [email protected]
Jesse BlackSoftware [email protected]@JesseBlack82