Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Swift Techniques for Testing

Swift Techniques for Testing

This talk explains two unique techniques to help make your tests easier to read, write and understand. The first technique is using Swift enums to abstract XCUIElementQuery calls and to isolate UI element into cases with each enum representing one screen. The second technique uses function builders to allow you to group multiple calls to XCTAssertTrue() and XCTAssertFalse().

Kaya Thomas

February 03, 2020
Tweet

More Decks by Kaya Thomas

Other Decks in Programming

Transcript

  1. How can we make our tests easier to write, easier

    to understand and less prone to developer errors?
  2. Features • Comparing three currencies to one base currency •

    Ability to change which currency is used for the base currency @kthomas901 dotSwift2020
  3. Features • Comparing three currencies to one base currency •

    Ability to change which currency is used for the base currency • Ability to enter the amount of money for the currency conversions @kthomas901 dotSwift2020
  4. Features • Comparing three currencies to one base currency •

    Ability to change which currency is used for the base currency • Ability to enter the amount of money for the currency conversions • Display the names of each country’s currency with their flags @kthomas901 dotSwift2020
  5. UI Testing with XCTestCase Search for existing UI elements on

    the XCUIApplication using: XCUIElementQuery
  6. UI Testing with XCTestCase Search for existing UI elements on

    the XCUIApplication using: XCUIElementQuery Example to find a label with the text “Hello World”: XCUIApplication().staticTexts[“Hello World”]
  7. Testing initial state func testInitialState() { XCTAssertTrue(app.staticTexts[“Base Currency"].exists) XCTAssertTrue(app.staticTexts["United States

    Dollar"].exists) XCTAssertTrue(app.textFields["1"].exists) } @kthomas901 dotSwift2020 XCTAssertTrue(app.staticTexts["Base Currency"].exists)
  8. Testing initial state func testInitialState() { XCTAssertTrue(app.staticTexts[“Base Currency"].exists) XCTAssertTrue(app.staticTexts["United States

    Dollar"].exists) XCTAssertTrue(app.textFields["1"].exists) } @kthomas901 dotSwift2020 XCTAssertTrue(app.staticTexts["United States Dollar"].exists)
  9. Testing initial state func testInitialState() { XCTAssertTrue(app.staticTexts["Base Currency"].exists) XCTAssertTrue(app.staticTexts["United States

    Dollar"].exists) XCTAssertTrue(app.textFields[“1"].exists) } @kthomas901 dotSwift2020 XCTAssertTrue(app.staticTexts["United States Dollar"].exists)
  10. Testing initial state func testInitialState() { XCTAssertTrue(app.staticTexts["Base Currency"].exists) XCTAssertTrue(app.staticTexts["United States

    Dollar"].exists) XCTAssertTrue(app.textFields[“1"].exists) } @kthomas901 dotSwift2020 XCTAssertTrue(app.textFields["1"].exists)
  11. Let’s capture our screen UI in an enum enum AppUI:

    String { case baseCurrencyView = "Base Currency" case unitedStatesDollarView = "United States Dollar" case euroView = "Euro" case canadianDollarView = "Canadian Dollar" case francView = "Franc" case initialTextField = "1" } @kthomas901 dotSwift2020
  12. Extending our enum extension AppUI { var isStaticText: Bool {

    return XCUIApplication().staticTexts[self.rawValue].exists } var isTextField: Bool { return XCUIApplication().textFields[self.rawValue].exists } }
  13. Revisiting initial state test func testInitialState() { } @kthomas901 dotSwift2020

    XCTAssertTrue(app.staticTexts[“Base Currency"].exists) XCTAssertTrue(app.staticTexts["United States Dollar"].exists) XCTAssertTrue(app.textFields[“1"].exists)
  14. Rewriting initial state test func testInitialState() { } @kthomas901 dotSwift2020

    XCTAssertTrue(app.staticTexts[“Base Currency"].exists) XCTAssertTrue(app.staticTexts["United States Dollar"].exists) XCTAssertTrue(app.textFields[“1"].exists)
  15. Rewriting initial state test func testInitialState() { } @kthomas901 dotSwift2020

    XCTAssertTrue(app.staticTexts[“Base Currency"].exists) XCTAssertTrue(app.staticTexts["United States Dollar"].exists) XCTAssertTrue(app.textFields[“1"].exists)
  16. Rewriting initial state test func testInitialState() { } @kthomas901 dotSwift2020

    XCTAssertTrue(app.staticTexts["United States Dollar"].exists) XCTAssertTrue(app.textFields[“1"].exists) XCTAssertTrue(AppUI.baseCurrencyView.isStaticText)
  17. Rewriting initial state test func testInitialState() { } @kthomas901 dotSwift2020

    XCTAssertTrue(app.staticTexts["United States Dollar"].exists) XCTAssertTrue(app.textFields[“1"].exists) XCTAssertTrue(AppUI.baseCurrencyView.isStaticText)
  18. Rewriting initial state test func testInitialState() { } @kthomas901 dotSwift2020

    XCTAssertTrue(app.textFields[“1"].exists) XCTAssertTrue(AppUI.baseCurrencyView.isStaticText) XCTAssertTrue(AppUI.unitedStatesDollarView.isStaticText)
  19. Rewriting initial state test func testInitialState() { } @kthomas901 dotSwift2020

    XCTAssertTrue(app.textFields[“1"].exists) XCTAssertTrue(AppUI.baseCurrencyView.isStaticText) XCTAssertTrue(AppUI.unitedStatesDollarView.isStaticText)
  20. Rewriting initial state test func testInitialState() { } @kthomas901 dotSwift2020

    XCTAssertTrue(AppUI.baseCurrencyView.isStaticText) XCTAssertTrue(AppUI.unitedStatesDollarView.isStaticText) XCTAssertTrue(AppUI.initialTextField.isTextField)
  21. Without using the enum func testUpdatingBaseCurrencyCell() { XCTAssertTrue(app.staticTexts["United States Dollar"].exists)

    XCTAssertFalse(app.staticTexts["Euro"].exists) app.tables.cells.element(boundBy: 0).tap() XCTAssertTrue(app.staticTexts["Euro"].exists) } @kthomas901 dotSwift2020
  22. Testing functionality using enum @kthomas901 dotSwift2020 func testUpdatingBaseCurrencyCell() { }

    XCTAssertTrue(app.staticTexts["United States Dollar"].exists) XCTAssertFalse(app.staticTexts["Euro"].exists) app.tables.cells.element(boundBy: 0).tap() XCTAssertTrue(app.staticTexts["Euro"].exists)
  23. Testing functionality using enum @kthomas901 dotSwift2020 func testUpdatingBaseCurrencyCell() { }

    XCTAssertTrue(app.staticTexts["United States Dollar"].exists) XCTAssertFalse(app.staticTexts["Euro"].exists) app.tables.cells.element(boundBy: 0).tap() XCTAssertTrue(app.staticTexts["Euro"].exists)
  24. Testing functionality using enum @kthomas901 dotSwift2020 func testUpdatingBaseCurrencyCell() { }

    XCTAssertFalse(app.staticTexts["Euro"].exists) app.tables.cells.element(boundBy: 0).tap() XCTAssertTrue(app.staticTexts["Euro"].exists) XCTAssertTrue(AppUI.unitedStatesDollarView.isStaticText)
  25. Testing functionality using enum @kthomas901 dotSwift2020 func testUpdatingBaseCurrencyCell() { }

    XCTAssertFalse(app.staticTexts["Euro"].exists) app.tables.cells.element(boundBy: 0).tap() XCTAssertTrue(app.staticTexts["Euro"].exists) XCTAssertTrue(AppUI.unitedStatesDollarView.isStaticText)
  26. Testing functionality using enum @kthomas901 dotSwift2020 func testUpdatingBaseCurrencyCell() { }

    XCTAssertFalse(app.staticTexts["Euro"].exists) app.tables.cells.element(boundBy: 0).tap() XCTAssertTrue(app.staticTexts["Euro"].exists) XCTAssertTrue(AppUI.unitedStatesDollarView.isStaticText)
  27. Testing functionality using enum @kthomas901 dotSwift2020 func testUpdatingBaseCurrencyCell() { }

    app.tables.cells.element(boundBy: 0).tap() XCTAssertTrue(app.staticTexts["Euro"].exists) XCTAssertTrue(AppUI.unitedStatesDollarView.isStaticText) XCTAssertFalse(AppUI.euroView.isStaticText)
  28. Testing functionality using enum @kthomas901 dotSwift2020 func testUpdatingBaseCurrencyCell() { }

    app.tables.cells.element(boundBy: 0).tap() XCTAssertTrue(app.staticTexts["Euro"].exists) XCTAssertTrue(AppUI.unitedStatesDollarView.isStaticText) XCTAssertFalse(AppUI.euroView.isStaticText)
  29. Testing functionality using enum @kthomas901 dotSwift2020 func testUpdatingBaseCurrencyCell() { }

    app.tables.cells.element(boundBy: 0).tap() XCTAssertTrue(app.staticTexts["Euro"].exists) XCTAssertTrue(AppUI.unitedStatesDollarView.isStaticText) XCTAssertFalse(AppUI.euroView.isStaticText)
  30. Testing functionality using enum @kthomas901 dotSwift2020 func testUpdatingBaseCurrencyCell() { }

    app.tables.cells.element(boundBy: 0).tap() XCTAssertTrue(AppUI.unitedStatesDollarView.isStaticText) XCTAssertFalse(AppUI.euroView.isStaticText) XCTAssertTrue(AppUI.euroView.isStaticText)
  31. Adding to the enum @kthomas901 dotSwift2020 extension AppUI { var

    isWithinTableCell: Bool { return XCUIApplication().tables.cells.buttons[self.rawValue].exists } var element: XCUIElement { if self.isStaticText { return XCUIApplication().staticTexts[self.rawValue] } else if self.isTextField { return XCUIApplication().textFields[self.rawValue] } else if self.isWithinTableCell { return XCUIApplication().tables.cells.buttons[self.rawValue] } fatalError() } }
  32. Adding to the enum @kthomas901 dotSwift2020 extension AppUI { var

    isWithinTableCell: Bool { return XCUIApplication().tables.cells.buttons[self.rawValue].exists } var element: XCUIElement { if self.isStaticText { return XCUIApplication().staticTexts[self.rawValue] } else if self.isTextField { return XCUIApplication().textFields[self.rawValue] } else if self.isWithinTableCell { return XCUIApplication().tables.cells.buttons[self.rawValue] } fatalError() } } var isWithinTableCell: Bool { return XCUIApplication().tables.cells.buttons[self.rawValue].exists }
  33. Adding to the enum @kthomas901 dotSwift2020 extension AppUI { var

    isWithinTableCell: Bool { return XCUIApplication().tables.cells.buttons[self.rawValue].exists } var element: XCUIElement { if self.isStaticText { return XCUIApplication().staticTexts[self.rawValue] } else if self.isTextField { return XCUIApplication().textFields[self.rawValue] } else if self.isWithinTableCell { return XCUIApplication().tables.cells.buttons[self.rawValue] } fatalError() } } var element: XCUIElement { } if self.isStaticText { return XCUIApplication().staticTexts[self.rawValue] }
  34. Adding to the enum @kthomas901 dotSwift2020 extension AppUI { var

    isWithinTableCell: Bool { return XCUIApplication().tables.cells.buttons[self.rawValue].exists } var element: XCUIElement { if self.isStaticText { return XCUIApplication().staticTexts[self.rawValue] } else if self.isTextField { return XCUIApplication().textFields[self.rawValue] } else if self.isWithinTableCell { return XCUIApplication().tables.cells.buttons[self.rawValue] } fatalError() } } var element: XCUIElement { } if self.isStaticText { return XCUIApplication().staticTexts[self.rawValue] }
  35. Adding to the enum @kthomas901 dotSwift2020 extension AppUI { var

    isWithinTableCell: Bool { return XCUIApplication().tables.cells.buttons[self.rawValue].exists } var element: XCUIElement { if self.isStaticText { return XCUIApplication().staticTexts[self.rawValue] } else if self.isTextField { return XCUIApplication().textFields[self.rawValue] } else if self.isWithinTableCell { return XCUIApplication().tables.cells.buttons[self.rawValue] } fatalError() } } var element: XCUIElement { } } else if self.isTextField { return XCUIApplication().textFields[self.rawValue] }
  36. Adding to the enum @kthomas901 dotSwift2020 extension AppUI { var

    isWithinTableCell: Bool { return XCUIApplication().tables.cells.buttons[self.rawValue].exists } var element: XCUIElement { if self.isStaticText { return XCUIApplication().staticTexts[self.rawValue] } else if self.isTextField { return XCUIApplication().textFields[self.rawValue] } else if self.isWithinTableCell { return XCUIApplication().tables.cells.buttons[self.rawValue] } fatalError() } } var element: XCUIElement { } } else if self.isTextField { return XCUIApplication().textFields[self.rawValue] }
  37. Adding to the enum @kthomas901 dotSwift2020 extension AppUI { var

    isWithinTableCell: Bool { return XCUIApplication().tables.cells.buttons[self.rawValue].exists } var element: XCUIElement { if self.isStaticText { return XCUIApplication().staticTexts[self.rawValue] } else if self.isTextField { return XCUIApplication().textFields[self.rawValue] } else if self.isWithinTableCell { return XCUIApplication().tables.cells.buttons[self.rawValue] } fatalError() } } var element: XCUIElement { } } else if self.isWithinTableCell { return XCUIApplication().tables.cells.buttons[self.rawValue] } fatalError()
  38. Adding to the enum @kthomas901 dotSwift2020 extension AppUI { var

    isWithinTableCell: Bool { return XCUIApplication().tables.cells.buttons[self.rawValue].exists } var element: XCUIElement { if self.isStaticText { return XCUIApplication().staticTexts[self.rawValue] } else if self.isTextField { return XCUIApplication().textFields[self.rawValue] } else if self.isWithinTableCell { return XCUIApplication().tables.cells.buttons[self.rawValue] } fatalError() } } var element: XCUIElement { } } else if self.isWithinTableCell { return XCUIApplication().tables.cells.buttons[self.rawValue] } fatalError()
  39. Revisiting the functionality test func testUpdatingBaseCurrencyCell() { XCTAssertTrue(AppUI.unitedStatesDollarView.isStaticText) XCTAssertTrue(AppUI.euroView.isStaticText) }

    @kthomas901 dotSwift2020 XCTAssertTrue(AppUI.euroView.isWithinTableCell) app.tables.cells.element(boundBy: 0).tap()
  40. Revisiting the functionality test func testUpdatingBaseCurrencyCell() { XCTAssertTrue(AppUI.unitedStatesDollarView.isStaticText) XCTAssertTrue(AppUI.euroView.isStaticText) }

    @kthomas901 dotSwift2020 app.tables.cells.element(boundBy: 0).tap() XCTAssertTrue(AppUI.euroView.isWithinTableCell)
  41. Revisiting the functionality test func testUpdatingBaseCurrencyCell() { XCTAssertTrue(AppUI.unitedStatesDollarView.isStaticText) XCTAssertTrue(AppUI.euroView.isStaticText) }

    @kthomas901 dotSwift2020 app.tables.cells.element(boundBy: 0).tap() XCTAssertTrue(AppUI.euroView.isWithinTableCell)
  42. Revisiting the functionality test func testUpdatingBaseCurrencyCell() { XCTAssertTrue(AppUI.unitedStatesDollarView.isStaticText) AppUI.euroView.element.tap() XCTAssertTrue(AppUI.euroView.isStaticText)

    } @kthomas901 dotSwift2020 XCTAssertTrue(AppUI.euroView.isWithinTableCell) XCTAssertTrue(AppUI.unitedStatesDollarView.isWithinTableCell)
  43. Creating an ElementType enum enum ElementType { case textField case

    staticText case tableCellButton init?(_ text: String) { if XCUIApplication().textFields[text].exists { self = .textField } else if XCUIApplication().staticTexts[text].exists { self = .staticText } else if XCUIApplication().tables.cells.buttons[text].exists { self = .tableCellButton } else { return nil } } } @kthomas901 dotSwift2020
  44. enum ElementType { case textField case staticText case tableCellButton init?(_

    text: String) { if XCUIApplication().textFields[text].exists { self = .textField } else if XCUIApplication().staticTexts[text].exists { self = .staticText } else if XCUIApplication().tables.cells.buttons[text].exists { self = .tableCellButton } else { return nil } } } @kthomas901 dotSwift2020 case textField case staticText case tableCellButton
  45. enum ElementType { case textField case staticText case tableCellButton init?(_

    text: String) { if XCUIApplication().textFields[text].exists { self = .textField } else if XCUIApplication().staticTexts[text].exists { self = .staticText } else if XCUIApplication().tables.cells.buttons[text].exists { self = .tableCellButton } else { return nil } } } @kthomas901 dotSwift2020 init?(_ text: String) { if XCUIApplication().textFields[text].exists { self = .textField } }
  46. enum ElementType { case textField case staticText case tableCellButton init?(_

    text: String) { if XCUIApplication().textFields[text].exists { self = .textField } else if XCUIApplication().staticTexts[text].exists { self = .staticText } else if XCUIApplication().tables.cells.buttons[text].exists { self = .tableCellButton } else { return nil } } } @kthomas901 dotSwift2020 init?(_ text: String) { if XCUIApplication().textFields[text].exists { self = .textField } }
  47. enum ElementType { case textField case staticText case tableCellButton init?(_

    text: String) { if XCUIApplication().textFields[text].exists { self = .textField } else if XCUIApplication().staticTexts[text].exists { self = .staticText } else if XCUIApplication().tables.cells.buttons[text].exists { self = .tableCellButton } else { return nil } } } @kthomas901 dotSwift2020 init?(_ text: String) { } } else if XCUIApplication().staticTexts[text].exists { self = .staticText }
  48. enum ElementType { case textField case staticText case tableCellButton init?(_

    text: String) { if XCUIApplication().textFields[text].exists { self = .textField } else if XCUIApplication().staticTexts[text].exists { self = .staticText } else if XCUIApplication().tables.cells.buttons[text].exists { self = .tableCellButton } else { return nil } } } @kthomas901 dotSwift2020 init?(_ text: String) { } } else if XCUIApplication().staticTexts[text].exists { self = .staticText }
  49. enum ElementType { case textField case staticText case tableCellButton init?(_

    text: String) { if XCUIApplication().textFields[text].exists { self = .textField } else if XCUIApplication().staticTexts[text].exists { self = .staticText } else if XCUIApplication().tables.cells.buttons[text].exists { self = .tableCellButton } else { return nil } } } @kthomas901 dotSwift2020 init?(_ text: String) { } } else if XCUIApplication().tables.cells.buttons[text].exists { self = .tableCellButton } else { return nil }
  50. enum ElementType { case textField case staticText case tableCellButton init?(_

    text: String) { if XCUIApplication().textFields[text].exists { self = .textField } else if XCUIApplication().staticTexts[text].exists { self = .staticText } else if XCUIApplication().tables.cells.buttons[text].exists { self = .tableCellButton } else { return nil } } } @kthomas901 dotSwift2020 init?(_ text: String) { } } else if XCUIApplication().tables.cells.buttons[text].exists { self = .tableCellButton } else { return nil }
  51. Get XCUIElement from ElementType extension AppUI { var element: XCUIElement

    { if self.isStaticText { return XCUIApplication().staticTexts[self.rawValue] } else if self.isTextField { return XCUIApplication().textFields[self.rawValue] } else if self.isWithinTableCell { return XCUIApplication().tables.cells.buttons[self.rawValue] } fatalError() }
  52. Get XCUIElement from ElementType extension ElementType { func getElement(with text:

    String) -> XCUIElement { switch self { case .staticText: return XCUIApplication().staticTexts[text] case .textField: return XCUIApplication().textFields[text] case .tableCellButton: return XCUIApplication().tables.cells.buttons[text] } } }
  53. Using ElementType extension AppUI { var type: ElementType { guard

    let type = ElementType(self.rawValue) else { fatalError() } return type } var element: XCUIElement { return type.getElement(with: self.rawValue) } }
  54. Using ElementType @kthomas901 dotSwift2020 extension AppUI { } var isStaticText:

    Bool { return XCUIApplication().staticTexts[self.rawValue].exists } var isTextField: Bool { return XCUIApplication().textFields[self.rawValue].exists } var isWithinTableCell: Bool { return XCUIApplication().tables.cells.buttons[self.rawValue].exists }
  55. Using ElementType @kthomas901 dotSwift2020 extension AppUI { } var isStaticText:

    Bool { return type == .staticText } var isTextField: Bool { return type == .textField } var isWithinTableCell: Bool { return type == .tableCellButton }
  56. Recap • Use the enum cases to represent the different

    elements on one screen @kthomas901 dotSwift2020
  57. Recap • Use the enum cases to represent the different

    elements on one screen • Have a computed variable for the element in your enum that you can take actions on @kthomas901 dotSwift2020
  58. Recap • Use the enum cases to represent the different

    elements on one screen • Have a computed variable for the element in your enum that you can take actions on @kthomas901 dotSwift2020 • Create a separate ElementType enum that can be reused for the different enums you have for each screen
  59. func testInitialState() { XCTAssertTrue(AppUI.baseCurrencyView.isStaticText) XCTAssertTrue(AppUI.unitedStatesDollarView.isStaticText) XCTAssertTrue(AppUI.initialTextField.isTextField) } func testInitialState() {

    assertTrue { AppUI.baseCurrencyView.isStaticText AppUI.unitedStatesDollarView.isStaticText AppUI.initialTextField.isTextField } } Before: After: @kthomas901 dotSwift2020
  60. Grouping XCAssertTrue & XCAssertFalse public func assertTrue(@BooleanFunctionBuilder builder: () ->

    [Bool]) { let expressions = builder() expressions.forEach { XCTAssertTrue($0) } } @kthomas901 dotSwift2020
  61. Grouping XCAssertTrue & XCAssertFalse public func assertTrue(@BooleanFunctionBuilder builder: () ->

    [Bool]) { let expressions = builder() expressions.forEach { XCTAssertTrue($0) } } public func assertFalse(@BooleanFunctionBuilder builder: () -> [Bool]) { let expressions = builder() expressions.forEach { XCTAssertFalse($0) } } @kthomas901 dotSwift2020
  62. Recap • Function builders help make your code more readable

    and easier to write @kthomas901 dotSwift2020
  63. Recap • Function builders help make your code more readable

    and easier to write • They can be really simple! @kthomas901 dotSwift2020
  64. Resources • iOS Automation with XCUITest, free course at Test

    Automation U by Shashikant Jagtap • Testing Swift by Paul Hudson • Awesome Function Builders Github repo @kthomas901 dotSwift2020