ios-templates
ios-templates copied to clipboard
[RFC] Add UITest ViewId and ScreenProtocol to template for easy UITest setup
Issue
-
Default UITest implication is hard to manage and not readable.
-
Affecting developer to not wanting to start writing UITest or not liking UITest.
Solution
- Include a boilerplate for easy adoption of UITest.
Example of Boilerplate code
enum ViewId {
case general(General)
func callAsFunction() -> String {
switch self {
case let .general(general): return general.rawValue
}
}
}
extension ViewId {
enum General: String {
case keyboard = "general.keyboard"
case loadingSpinner = "general.loading.spinner"
}
}
// UIKit
extension UIView {
func setAccessibilityId(_ viewId: ViewId) {
accessibilityIdentifier = viewId()
}
}
// SwiftUI
extension View {
func accessibility(_ viewId: ViewId) -> some View {
accessibilityIdentifier(viewId())
}
}
protocol ScreenProtocol: AnyObject {
associatedtype Identifier: RawRepresentable where Identifier.RawValue == String
var application: XCUIApplication { get }
}
extension ScreenProtocol {
var keyboard: KeyboardScreen {
KeyboardScreen(in: application)
}
func find(
_ elementKey: KeyPath<XCUIApplication, XCUIElementQuery>,
with identifier: Identifier
) -> XCUIElement {
return application[keyPath: elementKey][identifier.rawValue]
}
}
Who Benefits?
Template users.
What next?
- [ ] Vote if we want UITest boilerplate in Template.
- [ ] Discuss on UITest pattern.
- [ ] Include new UITest pattern to iOS Template as needed.
@blyscuit We mentioned about KIF in our compass, so I think it's better to combine both KIF and the boilerplate code.
@suho Let me research in to that. The built-in interaction functions and speed up seems nice, but if it adds too much complexity to the setup then we could create the lib ourself.
If we uses KIF, we can remove half of the boilerplate (XCUITest related) but we will need some KIF boilerplate.
@blyscuit You can check this one: https://github.com/nimblehq/redplanet-ios/tree/master/RedPlanetKIFTests
@suho Any idea why we stopped using KIF in our recent projects?
@blyscuit We did not include UI test into our recent projects, it's more related to the Team Lead who decides to add ui tests or not 😁
@blyscuit I'd love the idea, the example above and your POC are good. Thanks! I voted for both RFCs.
Some suggestions that I think might be cleaner.
// Bring this to View+Accessibility.swift
extension UIView {
func setAccessibilityId(_ viewId: ViewId) {
accessibilityIdentifier = viewId()
}
}
We're going to define ViewId
for screens:
extension ViewId {
enum General
enum Home
enum Login
...
}
And having separate extension files like:
// ViewId+General.swift
extension ViewId.General: String {
case keyboard = "general.keyboard"
case loadingSpinner = "general.loading.spinner"
}
// ViewId+Home.swift
extension ViewId.Home: String {
case profileButton = "home.profile.button"
case loadingSpinner = "home.loading.spinner"
}
// ViewId+Login.swift
extension ViewId.Login: String {
case loginButton = "login.login.button"
case emailField = "home.email.field"
}
@vnntsu Thank you. I will refactor them, these suggestion should be the norm 🙇 .
@vnntsu I ran in to this issue with adding case to extending enum

Best course is to extend ViewId
and declare the enum in a separate file. Which I updated in https://github.com/nimblehq/mvvm-rxswift-demo/pull/113/commits/984b09456e5f1ab6ac657a52e0f42585d2e56a2c.
// ViewId.swift
enum ViewId {
case login(Login)
case home(Home)
case general(General)
func callAsFunction() -> String {
switch self {
case let .general(general): return general.rawValue
case let .login(login): return login.rawValue
case let .home(home): return home.rawValue
}
}
}
// ViewId+General.swift
extension ViewId {
enum General: String {
case backButton = "Back"
}
}
I also tried other approaches but personally the current way is the best when:
- Setting accessibility ID, by limiting to enum of
ViewId
. - Writing test for particular screen, by assigning the enum cases of ViewId.
I'm open to more discussion to the approach.
Hi @blyscuit, I've checked your KIF POC. It's great 👏
In addition, we may need to add some extra verifications as improvements for the tests. We also need to check the content of the UI in case we can.
For example, we may want to check that the login button is enabled after the form was filled with valid data. Or we can verify the number on the label has the right format and so on. So I recommended adding expect()
.
let surveyImage = self.tester().waitForView(withAccessibilityIdentifier: ViewId.home(.surveyImage)())
expect(surveyImage).notTo(beNil())
let numberLabel = self.tester().waitForView(withAccessibilityIdentifier: ViewId.home(.numberLabel)()) as? UILabel
expect(numberLabel?.text) == "1,000"
let startButton = self.tester().waitForView(withAccessibilityIdentifier: ViewId.home(.startButton)()) as? UIButton
expect(startButton?.isEnabled) == true
I also want to shorten the function waitForView(withAccessibilityIdentifier:) as? UIButton
(as it's a bit too long for reading) to something like waitForButton(withId:)
. What do you think about it?
@blyscuit Add these functions can make writing more cool 🙏
extension KIFUITestActor {
func waitForView(withViewID viewID: ViewID) {
waitForView(withAccessibilityIdentifier: viewID())
}
func waitForTappableView(withViewID viewID: ViewID) {
waitForTappableView(withAccessibilityIdentifier: viewID())
}
func enterText(_ text: String, intoViewWithViewID viewID: ViewID) {
enterText(text, intoViewWithAccessibilityIdentifier: viewID())
}
func tapView(withViewID viewID: ViewID) {
tapView(withAccessibilityIdentifier: viewID())
}
}
// Writing test
self.tester().waitForView(withViewID: .splash(.background))
@markgravity I think we should use ViewID.toString
(in my POC I used callAsFunction() -> String
) instead of creating function for every KIF actions so that all KIF actions have the same code style and save the boilerplate code. What do you think?
@markgravity I think we should use
ViewID.toString
(in my POC I usedcallAsFunction() -> String
) instead of creating function for every KIF actions so that all KIF actions have the same code style and save the boilerplate code. What do you think?
@blyscuit From my recent experiment, repeating type ViewId
then ()
is so painful so that is why I wrote these functions 🤣
@markgravity I agree it is a pain. Maybe we can add this extension.
extension String {
static func viewId( _ viewId: ViewID) -> String { return viewId() }
}
tester().waitForView(withAccessibilityIdentifier: . viewId(.prPo(.view)))