爱因斯坦 是一个 UITest 框架,它通过 AccessibilityIdentifier 在整个项目与 UITest 之间集成业务逻辑。在 UITest 上,使用 EasyPredict 和 扩展 以更好地支持 UITest 代码编写。
比较示例
在 XCTestCase
中输入电话号码进行登录
👍 使用 Einstein ↓LoginAccessID.SignIn.phoneNumber.element .assertBreak(predicate: .exists(true))? .clearAndType(text: "MyPhoneNumber")
😵 不使用 Einstein ↓let element = app.buttons["LoginAccessID_SignIn_phoneNumber"] let predicate = NSPredicate(format: "exists == true") let promise = self.expectation(for: predicate, evaluatedWith: element, handler: nil) let result = XCTWaiter().wait(for: [promise], timeout: 10) if result == XCTWaiter.Result.completed { let stringValue = (element.value as? String) ?? "" let deleteString = stringValue.map { _ in XCUIKeyboardKey.delete.rawValue }.joined() element.typeText(deleteString) element.typeText("MyPhoneNumber") } else { assertionFailure("LoginAccessID_SignIn_phoneNumber element is't existe") }
文件结构
─┬─ Einstein
├─┬─ Identifier: -> `UIKit`
│ └─── AccessibilityIdentifier.swift
│
└─┬─ UITest: -> `Einstein/Identifier` & `XCTest` & `Then`
├─┬─ Model
│ ├─── EasyPredicate.swift
│ └─── Springboard.swift
└─┬─ Extensions
├─── RawRepresentable+helpers.swift
├─── PrettyRawRepresentable+helpers.swift
├─── XCTestCase+helpers.swift
├─── XCUIElement+helpers.swift
└─── XCUIElementQuery+helpers.swift
安装
需要
iOS >= 9.0
和Swift5.0
,并使用 Cocoapods
target 'XXXProject' do
# in project target
pod 'Einstein/Identifier'
target 'XXXProjectUITests' do
# in UITest target
pod 'Einstein'
end
end
使用
- AccessibilityIdentifier
- 项目目标
- UITest 目标
- 在 UITest 中应用
- EasyPredicate
- 扩展
1. 无障碍标识符
说明
所有UIKit的无障碍标识符都是协议UIAccessibilityIdentification
的一个属性,所有枚举的rawValue默认遵循RawRepresentable
展开查看详细信息
- 1.1 定义枚举
- 设置String中的rawValue
- 根据需要添加PrettyRawRepresentable
- 1.2 使用枚举的rawValue设置UIKit的无障碍标识符
- 方法1:后缀操作符
- 方法2:UIAccessibilityIdentification的扩展
- 1.3 在UITest目标中应用
1.1 定义枚举
struct LoginAccessID {
enum SignIn: String {
case signIn, phoneNumber, password
}
enum SignUp: String {
case signUp, phoneNumber
}
enum Forget: String, PrettyRawRepresentable {
case phoneNumber // and so on
}
}
强烈建议在枚举上添加PrettyRawRepresentable
协议,这样你将获得带属性路径的RawValue字符串,以避免在不同页面中无障碍标识符相同。
// for example:
let str1 = LoginAccessID.SignIn.phoneNumber
let str2 = LoginAccessID.SignUp.phoneNumber
let str3 = LoginAccessID.Forget.phoneNumber // had add PrettyRawRepresentable
str1 == "phoneNumber"
str2 == "phoneNumber"
str3 == "LoginAccessID_Forget_phoneNumber"
1.2 使用枚举的rawValue设置UIKit的无障碍标识符
// system way
signInPhoneTextField.accessibilityIdentifier = "LoginAccessID_SignIn_phoneNumber"
// define infix operator <<<
forgetPhoneTextField <<< LoginAccessID.Forget.phoneNumber
print(forgetPhoneTextField.accessibilityIdentifier)
// "LoginAccessID_Forget_phoneNumber"
1.3. 在UITest目标中应用
说明
首先在UITest中导入定义的枚举文件
- 方法1:在XXXProject和XXXUITest中将它的
target membership
设置为true- 方法2:在UITest中使用@testable导入项目文件(链接:如何设置)
@testable import XXXPreject
// extension the protocol RawRepresentable and it's RawValue == String
typealias SignInPage = LoginAccessID.SignIn
// type the phone number
SignInPage.phoneNumber.element.waitUntilExists().clearAndType(text: "myPhoneNumber")
// type passward
SignInPage.password.element.clearAndType(text: "******")
// start login
SignInPage.signIn.element.assert(predicate: .isEnabled(true)).tap()
2. EasyPredicate
说明
EasyPredicate的RawValue是PredicateRawValue
(用于管理逻辑和转换NSPredicate的另一个枚举)。展开查看EasyPredicate的案例
public enum EasyPredicate: RawRepresentable { case exists(_ exists: Bool) case isEnabled(_ isEnabled: Bool) case isHittable(_ isHittable: Bool) case isSelected(_ isSelected: Bool) case label(_ comparison: Comparison, _ value: String) case identifier(_ identifier: String) case type(_ type: XCUIElement.ElementType) case other(_ ragular: String) }
尽管 NSPredicate
功能强大,但开发程序接口不太理想,我们可以尝试将硬编码风格转换为面向对象风格,这正是 EasyPredicate 做的事情。
// use EasyPredicate
let targetElement = query.filter(predicate: .label(.beginsWith, "abc")).element
// use NSPredicate
let predicate = NSPredicate(format: "label BEGINSWITH 'abc'")
let targetElement = query.element(matching: predicate).element
EasyPredicate 合并
// "elementType == 0 && exists == true && label BEGINSWITH 'abc'"
let predicate: EasyPredicate = [.type(.button), .exists(true), .label(.beginsWith, "abc")].merged()
// "elementType == 0 || exists == true || label BEGINSWITH 'abc'"
let predicate: EasyPredicate = [.type(.button), .exists(true), .label(.beginsWith, "abc")].merged(withLogic: .or)
3. UITest 扩展
3.1 String 扩展
/*
Note: string value can be a RawRepresentable and String at the same time
for example:
`let element: XCUIElement = "SomeString".element`
*/
extension String: RawRepresentable {
public var rawValue: String { return self }
public init?(rawValue: String) {
self = rawValue
}
}
3.2 RawRepresentable 扩展
为元素为 RawRepresentable 的序列扩展
public extension Sequence where Element: RawRepresentable, Element.RawValue == String {
/// get the elements which match with identifiers and predicates limited in timeout
///
/// - Parameters:
/// - predicates: predicates as the match rules
/// - logic: relation of predicates
/// - timeout: if timeout == 0, return the elements immediately otherwise retry until timeout
/// - Returns: get the elements
func elements(predicates: [EasyPredicate], logic: NSCompoundPredicate.LogicalType, timeout: Int) -> [XCUIElement] {}
/// get the first element was matched predicate
func anyElement(predicate: EasyPredicate) -> XCUIElement? {}
}
为 RawRepresentable 扩展扩展
/*
Get the `XCUIElement` from RawRepresentable's RawValue which also been used as accessibilityIdentifier
*/
public extension RawRepresentable where RawValue == String {
var element: XCUIElement {}
var query: XCUIElementQuery {}
var count: Int {}
subscript(i: Int) -> XCUIElement {}
func queryFor(identifier: Self) -> XCUIElementQuery {}
}
3.3 扩展 XCUIElement
为 XCUIElement (基本) 扩展
public extension PredicateBaseExtensionProtocol where Self == T {
/// create a new preicate with EasyPredicates and LogicalType to judge is it satisfied on self
///
/// - Parameters:
/// - predicates: predicates rules
/// - logic: predicates relative
/// - Returns: tuple of result and self
@discardableResult
func waitUntil(predicates: [EasyPredicate], logic: NSCompoundPredicate.LogicalType = .and, timeout: TimeInterval = 10, handler: XCTNSPredicateExpectation.Handler? = nil) -> (result: XCTWaiter.Result, element: T) {
if predicates.count <= 0 { fatalError("predicates cannpt be empty!") }
let test = XCTestCase().then { $0.continueAfterFailure = true }
let promise = test.expectation(for: predicates.toPredicate(logic), evaluatedWith: self, handler: handler)
let result = XCTWaiter().wait(for: [promise], timeout: timeout)
return (result, self)
}
/// assert by new preicate with EasyPredicates and LogicalType, if assert is passed then return self or return nil
///
/// - Parameters:
/// - predicates: rules
/// - logic: predicates relative
/// - Returns: self or nil
@discardableResult
func assertBreak(predicates: [EasyPredicate], logic: NSCompoundPredicate.LogicalType = .and) -> T? {
if predicates.first == nil { fatalError("❌ predicates can't be empty") }
let filteredElements = ([self] as NSArray).filtered(using: predicates.toPredicate(logic))
if filteredElements.isEmpty {
let predicateStr = predicates.map { "\n <\($0.rawValue.regularString)>" }.joined()
assertionFailure("❌ \(self) is not satisfied logic:\(logic) about rules: \(predicateStr)")
}
return filteredElements.isEmpty ? nil : self
}
}
为 XCUIElement 基础扩展扩展
// MARK: - wait
@discardableResult
func waitUntil(predicate: EasyPredicate, timeout: TimeInterval = 10, handler: XCTNSPredicateExpectation.Handler? = nil) -> (result: XCTWaiter.Result, element: XCUIElement) {}
@discardableResult
func waitUntilExists(timeout: TimeInterval = 10) -> (result: XCTWaiter.Result, element: XCUIElement) {}
@discardableResult
func wait(_ s: UInt32 = 1) -> XCUIElement {}
// MARK: - assert
@discardableResult
func assertBreak(predicate: EasyPredicate) -> XCUIElement? {}
@discardableResult
func assert(predicate: EasyPredicate) -> XCUIElement {}
@discardableResult
func waitUntilExistsAssert(timeout: TimeInterval = 10) -> XCUIElement {}
@discardableResult
func assert(predicate: EasyPredicate, timeout: TimeInterval = 10) -> XCUIElement {}
为 XCUIElement 自定义扩展扩展
// MARK: - Extension
public extension XCUIElement {
/// get the results in the descendants which matching the EasyPredicates
///
/// - Parameters:
/// - predicates: EasyPredicate's rules
/// - logic: rule's relate
/// - Returns: result target
@discardableResult
func descendants(predicates: [EasyPredicate], logic: NSCompoundPredicate.LogicalType = .and) -> XCUIElementQuery {}
@discardableResult
func descendants(predicate: EasyPredicate) -> XCUIElementQuery {}
/// Returns a query for direct children of the element matching with EasyPredicates
///
/// - Parameters:
/// - predicates: EasyPredicate rules
/// - logic: rules relate
/// - Returns: result query
@discardableResult
func children(predicates: [EasyPredicate], logic: NSCompoundPredicate.LogicalType = .and) -> XCUIElementQuery {}
@discardableResult
func children(predicate: EasyPredicate) -> XCUIElementQuery {}
/// Wait until it's available and then type a text into it.
@discardableResult
func tapAndType(text: String, timeout: TimeInterval = 10) -> XCUIElement {}
/// Wait until it's available and clear the text, then type a text into it.
@discardableResult
func clearAndType(text: String, timeout: TimeInterval = 10) -> XCUIElement {}
@discardableResult
func hidenKeyboard(inApp: XCUIApplication) -> XCUIElement {}
@discardableResult
func setSwitch(on: Bool, timeout: TimeInterval = 10) -> XCUIElement {}
@discardableResult
func forceTap(timeout: TimeInterval = 10) -> XCUIElement {}
@discardableResult
func tapIfExists(timeout: TimeInterval = 10) -> XCUIElement {}
}
为序列:XCUIElement 扩展扩展
extension Sequence where Element: XCUIElement {
/// get the elements which match with identifiers and predicates limited in timeout
///
/// - Parameters:
/// - predicates: predicates as the match rules
/// - logic: relation of predicates
/// - timeout: if timeout == 0, return the elements immediately otherwise retry until timeout
/// - Returns: get the elements
func elements(predicates: [EasyPredicate], logic: NSCompoundPredicate.LogicalType, timeout: Int) -> [Element] {}
/// get the first element was matched predicate
func anyElement(predicate: EasyPredicate) -> Element? {}
}
3.4 扩展 XCUIElementQuery
为 XCUIElementQuery 扩展扩展
public extension XCUIElementQuery {
/// safe to get index
///
/// - Parameter index: index
/// - Returns: optional element
func element(safeIndex index: Int) -> XCUIElement? { }
/// asset empty of query
///
/// - Parameter empty: bool value
/// - Returns: optional query self
func assertEmpty(empty: Bool = false) -> XCUIElementQuery? { }
/// get the results which matching the EasyPredicates
///
/// - Parameters:
/// - predicates: EasyPredicate's rules
/// - logic: rules relate
/// - Returns: ElementQuery
func matching(predicates: [EasyPredicate], logic: NSCompoundPredicate.LogicalType = .and) -> XCUIElementQuery { }
func matching(predicate: EasyPredicate) -> XCUIElementQuery { }
/// get the taget element which matching the EasyPredicates
///
/// - Parameters:
/// - predicates: EasyPredicate's rules
/// - logic: rule's relate
/// - Returns: result target
func element(predicates: [EasyPredicate], logic: NSCompoundPredicate.LogicalType = .and) -> XCUIElement { }
func element(predicate: EasyPredicate) -> XCUIElement { }
/// get the results in the query's descendants which matching the EasyPredicates
///
/// - Parameters:
/// - predicates: EasyPredicate's rules
/// - logic: rule's relate
/// - Returns: result target
func descendants(predicates: [EasyPredicate], logic: NSCompoundPredicate.LogicalType = .and) -> XCUIElementQuery { }
func descendants(predicate: EasyPredicate) -> XCUIElementQuery { }
/// filter the query by rules to create new query
///
/// - Parameters:
/// - predicates: EasyPredicate's rules
/// - logic: rule's relate
/// - Returns: result target
func containing(predicates: [EasyPredicate], logic: NSCompoundPredicate.LogicalType = .and) -> XCUIElementQuery { }
func containing(predicate: EasyPredicate) -> XCUIElementQuery { }
}
3.5 扩展 XCTestCase
为 XCTestCase (运行时) 扩展
/**
associated object
*/
public extension XCTestCase {
private struct XCTestCaseAssociatedKey {
static var app = 0
}
var app: XCUIApplication {
set {
objc_setAssociatedObject(self, &XCTestCaseAssociatedKey.app, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
get {
let _app = objc_getAssociatedObject(self, &XCTestCaseAssociatedKey.app) as? XCUIApplication
guard let app = _app else { return XCUIApplication().then { self.app = $0 } }
return app
}
}
}
为 XCTestCase 扩展扩展
public extension XCTestCase {
// MARK: - methods
func isSimulator() -> Bool {}
func takeScreenshot(activity: XCTActivity, name: String = "Screenshot") {}
func takeScreenshot(groupName: String = "--- Screenshot ---", name: String = "Screenshot") {}
func group(text: String = "Group", closure: (_ activity: XCTActivity) -> ()) {}
func hideAlertsIfNeeded() {}
func setAirplane(_ value: Bool) {}
func deleteMyAppIfNeed() {}
/// Try to force launch the application. This structure tries to ovecome the issues described at https://forums.developer.apple.com/thread/15780
func tryLaunch<T: RawRepresentable>(arguments: [T], count counter: Int = 10, wait: UInt32 = 2) where T.RawValue == String {}
func tryLaunch(count counter: Int = 10) {}
func killAppAndRelaunch() {}
/// Try to force closing the application
func tryTearDown(wait: UInt32 = 2) {}
}
作者
XcodeYang, [email protected]
许可证
Einstein项目受MIT许可证许可。有关更多信息,请参阅LICENSE文件。