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

Нативные UI тесты

CocoaHeads
April 26, 2018
620

Нативные UI тесты

CocoaHeads

April 26, 2018
Tweet

More Decks by CocoaHeads

Transcript

  1. План • Зачем • Hello, world! пример на XCUI •

    Наша реализация функциональных тестов • Базовая часть • Оптимизации • CI/CD, процессы и прочее !2
  2. Инструменты Black White Open Source Поддержка Swift & Obj-C Android

    EarlGrey ❌ ✅ ✅ ✅ ✅ ❌ XCUI ✅ ❌ ❌ ✅ ✅ ❌ Appium ✅ ❌ ✅ ✅ ❌ ✅ Calabash ✅ ❌ ✅ ❌ ❌ ✅ KIF ✅ ❌ ✅ ✅ ✅ ❌ MonkeyTalk ✅ ❌ ❌ ❌ ❌ ✅ Frank ✅ ❌ ✅ ❌ ❌ ❌ !8
  3. Инструменты Black White Open Source Поддержка Swift & Obj-C Android

    EarlGrey ❌ ✅ ✅ ✅ ✅ ❌ XCUI ✅ ❌ ❌ ✅ ✅ ❌ Appium ✅ ❌ ✅ ✅ ❌ ✅ Calabash ✅ ❌ ✅ ❌ ❌ ✅ KIF ❌ ✅ ✅ ✅ ✅ ❌ MonkeyTalk ✅ ❌ ❌ ❌ ❌ ✅ Frank ✅ ❌ ✅ ❌ ❌ ❌ !9
  4. !11

  5. !12

  6. !13

  7. !15

  8. !16

  9. !17

  10. Открывается экран со скроллвью Тап приводит к тому, что происходит

    программный скролл Реально доскроллить до кота нельзя Соответственно тапнуть тоже нельзя !19
  11. Просто экран с котиком Мы используем его для определения того,

    что мы можем использовать в XCUI для определения видимости !20
  12. extension XCUIElement { func isWithin(_ other: XCUIElement) -> Bool {

    return frame.width > 0 && frame.height > 0 && frame.intersection(other.frame) == frame } } func test_showingThat_visibilityCheckCanLeadToFalseNegative() { app.tables.buttons["Видимая вьюшка"].tap() XCTAssert(app.images.element(boundBy: 0).exists) XCTAssert(app.images.element(boundBy: 0).isWithin(app)) XCTAssert(app.images.element(boundBy: 0).isHittable) } !21
  13. !22

  14. !23

  15. !24

  16. !25

  17. !26

  18. !27

  19. !31

  20. Что у нас по факту • 150 тестовых сценариев, 200

    тестов • 4 девайса, 3 оси, регрессионный суит за 2.5 часа • Интеграция в релизный процесс • Полный прогон каждый день • Несколько тестов на каждый PR (блокирующий билд) • 80% зеленых на iOS 11, 50% по всем осям !32
  21. Наша реализация • Пример теста • Page Object • Поиск

    элемента • Проверка на видимость • Скроллинг • Действия и проверки !34
  22. !35

  23. func test() { precondition { initialState.launchAuthorized(user: resourceManager.user()) } step("В NavBar

    тапнуть на имя пользователя") { pageObjects.account.userNameInNavigationBar.tap() assert("Открывается экран профиля") { pageObjects.profileScreen.view.assert.isDisplayed() } } step("Тапнуть на \"Выйти\".") { pageObjects.profileScreen.logout.tap() assert("Открывается экран кабинета \"Войдите или зарегистрируйтесь\"") { pageObjects.unauthorizedUserAccount.view.assert.isDisplayed() } } } !36
  24. func test() { precondition { initialState.launchAuthorized(user: resourceManager.user()) } step("В NavBar

    тапнуть на имя пользователя") { pageObjects.account.userNameInNavigationBar.tap() assert("Открывается экран профиля") { pageObjects.profileScreen.view.assert.isDisplayed() } } step("Тапнуть на \"Выйти\".") { pageObjects.profileScreen.logout.tap() assert("Открывается экран кабинета \"Войдите или зарегистрируйтесь\"") { pageObjects.unauthorizedUserAccount.view.assert.isDisplayed() } } } !37
  25. func test() { precondition { initialState.launchAuthorized(user: resourceManager.user()) } step("В NavBar

    тапнуть на имя пользователя") { pageObjects.account.userNameInNavigationBar.tap() assert("Открывается экран профиля") { pageObjects.profileScreen.view.assert.isDisplayed() } } step("Тапнуть на \"Выйти\".") { pageObjects.profileScreen.logout.tap() assert("Открывается экран кабинета \"Войдите или зарегистрируйтесь\"") { pageObjects.unauthorizedUserAccount.view.assert.isDisplayed() } } } !38
  26. func test() { precondition { initialState.launchAuthorized(user: resourceManager.user()) } step("В NavBar

    тапнуть на имя пользователя") { pageObjects.account.userNameInNavigationBar.tap() assert("Открывается экран профиля") { pageObjects.profileScreen.view.assert.isDisplayed() } } step("Тапнуть на \"Выйти\".") { pageObjects.profileScreen.logout.tap() assert("Открывается экран кабинета \"Войдите или зарегистрируйтесь\"") { pageObjects.unauthorizedUserAccount.view.assert.isDisplayed() } } } !39
  27. Наша реализация • Пример теста • Page Object • Поиск

    элемента • Проверка на видимость • Скроллинг • Действия и проверки !40
  28. Page Object • Page Object - набор элементов (экран) •

    Page Object Element • Локатор • Проверки • Действия !41
  29. !42

  30. !43

  31. !44

  32. Наша реализация • Пример теста • Page Object • Поиск

    элемента • Проверка на видимость • Скроллинг • Действия и проверки !45
  33. @available(iOS 3.0, *) open class NSPredicate : NSObject, NSSecureCoding, NSCopying

    { public init(format predicateFormat: String, argumentArray arguments: [Any]?) public init(format predicateFormat: String, arguments argList: CVaListPointer) public init(value: Bool) @available(iOS 4.0, *) public init(block: @escaping (Any?, [String : Any]?) -> Bool) XCUIElementAttributes !48
  34. !50

  35. Наша реализация • Пример теста • Page Object • Поиск

    элемента • Проверка на видимость • Скроллинг • Действия и проверки !51
  36. SBTUITestTunnel SBTUITestTunnelServer.registerCustomCommandNamed("getValue") { request in return "value" } let value

    = application.performCustomCommandNamed( "getValue", object: "view_12345" ) В тестах: !55
  37. Для чего используем • Проверка на видимость • Скроллинг •

    Идентификатор для AB-тестов • Открытие диплинков • Симуляция геопозиции • Валидация запросов к АПИ • Подмена запросов АПИ !56
  38. Дополнительные данные в Accessibility иерархии Классы Селектор iOS 11 NSObject,

    UIAccessibilityTextFieldElement _accessibilityAXAttributedValue iOS 9, iOS 10 Все наследники NSObject accessibilityValue #2 !57
  39. Наша реализация • Пример теста • Page Object • Поиск

    элемента • Проверка на видимость • Скроллинг • Действия и проверки !58
  40. Патч координат func tappableCoordinate(x: CGFloat, y: CGFloat) -> XCUICoordinate {

    var x = x var y = y let frame = self.frame // TODO: Брать значение 1 раз за сессию (кешировать) let minX: CGFloat = 0 // С -1 работает как с 0 let minY: CGFloat = 20 // С 19 не работает вообще let maxX: CGFloat = frame.width - 1 // Без -1 делится на 2 let maxY: CGFloat = frame.height - 1 // Без -1 делится на 2 if x > maxX { x = maxX } if y > maxY { y = maxY } if x < minX { x = minX } if y < minY { y = minY } return coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0)) .withOffset(CGVector(dx: x, dy: y)) } !60
  41. Свиззлинг Collection View swizzle( #selector(UICollectionView.reloadData), #selector(UICollectionView.swizzled_CollectionViewSwizzler_reloadData) ) swizzle( #selector(UICollectionView.performBatchUpdates(_:completion:)), #selector(UICollectionView.swizzled_CollectionViewSwizzler_performBatchUpdates(_:completion:))

    ) swizzle( #selector(UICollectionView.accessibilityElementCount), #selector(UICollectionView.swizzled_CollectionViewSwizzler_accessibilityElementCount) ) swizzle( #selector(UICollectionView.accessibilityElement(at:)), #selector(UICollectionView.swizzled_CollectionViewSwizzler_accessibilityElement(at:)) ) swizzle( #selector(UICollectionView.index(ofAccessibilityElement:)), #selector(UICollectionView.swizzled_CollectionViewSwizzler_index(ofAccessibilityElement:)) ) #3 !62
  42. Свиззлинг Collection View swizzle( #selector(UICollectionView.reloadData), #selector(UICollectionView.swizzled_CollectionViewSwizzler_reloadData) ) swizzle( #selector(UICollectionView.performBatchUpdates(_:completion:)), #selector(UICollectionView.swizzled_CollectionViewSwizzler_performBatchUpdates(_:completion:))

    ) swizzle( #selector(UICollectionView.accessibilityElementCount), #selector(UICollectionView.swizzled_CollectionViewSwizzler_accessibilityElementCount) ) swizzle( #selector(UICollectionView.accessibilityElement(at:)), #selector(UICollectionView.swizzled_CollectionViewSwizzler_accessibilityElement(at:)) ) swizzle( #selector(UICollectionView.index(ofAccessibilityElement:)), #selector(UICollectionView.swizzled_CollectionViewSwizzler_index(ofAccessibilityElement:)) ) #3 !63
  43. Свиззлинг Collection View swizzle( #selector(UICollectionView.reloadData), #selector(UICollectionView.swizzled_CollectionViewSwizzler_reloadData) ) swizzle( #selector(UICollectionView.performBatchUpdates(_:completion:)), #selector(UICollectionView.swizzled_CollectionViewSwizzler_performBatchUpdates(_:completion:))

    ) swizzle( #selector(UICollectionView.accessibilityElementCount), #selector(UICollectionView.swizzled_CollectionViewSwizzler_accessibilityElementCount) ) swizzle( #selector(UICollectionView.accessibilityElement(at:)), #selector(UICollectionView.swizzled_CollectionViewSwizzler_accessibilityElement(at:)) ) swizzle( #selector(UICollectionView.index(ofAccessibilityElement:)), #selector(UICollectionView.swizzled_CollectionViewSwizzler_index(ofAccessibilityElement:)) ) #3 !64
  44. Наша реализация • Пример теста • Page Object • Поиск

    элемента • Проверка на видимость • Скроллинг • Действия и проверки !65
  45. Действия func tap(actionSettings: ActionSettings) { return perform(actionSettings: actionSettings) { (element:

    XCUIElement, snapshot: ElementSnapshot) -> InteractionSpecificResult in if element.isHittable { element.tap() } else { element.center.tap() } return .success } } !66
  46. Проверки func hasValue(_ expectedValue: String, checkSettings: CheckSettings) -> Bool {

    return performCheck(checkSettings: checkSettings) { (element: XCUIElement, snapshot: ElementSnapshot) -> InteractionSpecificResult in let elementValue = EnchancedAccessibilityValue .fromAccessibilityValue(snapshot.value as? String)? .originalValue if elementValue == expectedValue { return .success } else { return .failureWithMessage("ожидалось: \(expectedValue), актуальное: \(elementValue)") } } } !67
  47. Проверки func hasValue(_ expectedValue: String, checkSettings: CheckSettings) -> Bool {

    return performCheck(checkSettings: checkSettings) { (element: XCUIElement, snapshot: ElementSnapshot) -> InteractionSpecificResult in let elementValue = EnchancedAccessibilityValue .fromAccessibilityValue(snapshot.value as? String)? .originalValue if elementValue == expectedValue { return .success } else { return .failureWithMessage("ожидалось: \(expectedValue), актуальное: \(elementValue)") } } } !68
  48. Переход по диплинкам func test() { let advertisement = resourceManager.advertisement()

    let detailedAdvertisement = resourceManager.detailedAdvertisement(id: advertisement.id) precondition( """ Запустить приложение и перейти на Главную. Открыть карточку объявления. """) { initialState.openAdvertisementViaDeepLink(advertisement) } !71
  49. Переход по диплинкам static func takeOff() { SBTUITestTunnelServer.registerCustomCommandNamed("openDeepLink") { request

    in return handleOpenDeepLinkRequest(request: request) } } ... private static func openDeepLink(deepLinkUri: String) -> DeepLinkOpeningResult { if let url = URL(string: deepLinkUri) { if UIApplication.shared.openURL(url) { return .success } else { return .failure("Не удалось открыть диплинк '\(deepLinkUri)', убедитесь в его валидности") } } else { return .failure("URL не валиден: \(deepLinkUri)") } } !72
  50. Переход по диплинкам static func takeOff() { SBTUITestTunnelServer.registerCustomCommandNamed("openDeepLink") { request

    in return handleOpenDeepLinkRequest(request: request) } } ... private static func openDeepLink(deepLinkUri: String) -> DeepLinkOpeningResult { if let url = URL(string: deepLinkUri) { if UIApplication.shared.openURL(url) { return .success } else { return .failure("Не удалось открыть диплинк '\(deepLinkUri)', убедитесь в его валидности") } } else { return .failure("URL не валиден: \(deepLinkUri)") } } !73
  51. Кеширование XCElementSnapshot let elementQueryResolvingState = ElementQueryResolvingState() let xcuiElementQuery = XCUIApplication().any.matching(

    NSPredicate( block: { snapshot, _ -> Bool in if let snapshot = snapshot as? XCElementSnapshot { let elementSnapshot = ElementSnapshotImpl(snapshot) let matchingResult = elementSnapshotMatcher.matches(snapshot: elementSnapshot) elementQueryResolvingState.append( matchingResult: matchingResult, elementSnapshot: elementSnapshot ) return matchingResult.matched } else { return false } } ) ) #4 !74
  52. Кеширование XCElementSnapshot let elementQueryResolvingState = ElementQueryResolvingState() let xcuiElementQuery = XCUIApplication().any.matching(

    NSPredicate( block: { snapshot, _ -> Bool in if let snapshot = snapshot as? XCElementSnapshot { let elementSnapshot = ElementSnapshotImpl(snapshot) let matchingResult = elementSnapshotMatcher.matches(snapshot: elementSnapshot) elementQueryResolvingState.append( matchingResult: matchingResult, elementSnapshot: elementSnapshot ) return matchingResult.matched } else { return false } } ) ) #4 !75
  53. Кеширование XCElementSnapshot func resolveElement(_ closure: (XCUIElementQuery) -> (XCUIElement)) -> ResolvedElementQuery

    { let stepLogBefore = StepLogBefore.other("Поиск элемента") let resolvedElementQuery = stepLogger.logStep(stepLogBefore: stepLogBefore) { () -> StepLoggerWrappedResult<ResolvedElementQuery> in let element = closure(xcuiElementQuery) elementQueryResolvingState.start() let elementExists = element.exists elementQueryResolvingState.stop() !76
  54. Очистка стейта • Настройки клавиатур • Настройки алерта об алтернативных

    клавиатурах • Удаление аппа • Сброс настроек уведомлений, камеры, галереи • Сброс настроек геолокации !77
  55. Симулирование геолокации CoreLocation.framework CLSimulationManager SBTUITestTunnelServer.registerCustomCommandNamed("simulateLocation") { request in guard let

    location = request as? CLLocation else { return NSNumber(booleanLiteral: false) } LocationSimulationManager.shared.stopLocationSimulation() LocationSimulationManager.shared.clearSimulatedLocations() LocationSimulationManager.shared.appendSimulatedLocation(location) LocationSimulationManager.shared.flush() LocationSimulationManager.shared.startLocationSimulation() return NSNumber(booleanLiteral: true) } #5 !78
  56. Запуск тестов не зависает не крешится запускает все тесты xcodebuild

    / xctestrun ❌ ❌ ❌ bluepill ✅ ✅ ❌ Наш раннер ✅ ✅ ✅ !89
  57. Итог • E2E тестирование iOS реально • XCUI не выполняет

    всех задач • Нужно инвестировать много ресурсов !92