Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Add UITest ViewId and ScreenProtocol to template for easy UITest setup #397

Open
3 tasks
blyscuit opened this issue Dec 1, 2022 · 15 comments
Open
3 tasks

Comments

@blyscuit
Copy link
Collaborator

blyscuit commented Dec 1, 2022

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.
@suho
Copy link
Member

suho commented Dec 1, 2022

@blyscuit We mentioned about KIF in our compass, so I think it's better to combine both KIF and the boilerplate code.

@blyscuit
Copy link
Collaborator Author

blyscuit commented Dec 1, 2022

Vote on adding UITest ViewId

Vote on KIF

@blyscuit
Copy link
Collaborator Author

blyscuit commented Dec 1, 2022

@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.

@blyscuit
Copy link
Collaborator Author

If we uses KIF, we can remove half of the boilerplate (XCUITest related) but we will need some KIF boilerplate.

@suho
Copy link
Member

suho commented Dec 19, 2022

@blyscuit You can check this one: https://github.com/nimblehq/redplanet-ios/tree/master/RedPlanetKIFTests

@blyscuit
Copy link
Collaborator Author

@suho Any idea why we stopped using KIF in our recent projects?

@suho
Copy link
Member

suho commented Dec 19, 2022

@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 😁

@vnntsu
Copy link
Contributor

vnntsu commented Dec 20, 2022

@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"
}

@blyscuit
Copy link
Collaborator Author

@vnntsu Thank you. I will refactor them, these suggestion should be the norm 🙇 .

@blyscuit
Copy link
Collaborator Author

@vnntsu I ran in to this issue with adding case to extending enum

Screen Shot 2022-12-21 at 15 47 17

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:

  1. Setting accessibility ID, by limiting to enum of ViewId.
  2. Writing test for particular screen, by assigning the enum cases of ViewId.

I'm open to more discussion to the approach.

@nmint8m
Copy link
Contributor

nmint8m commented Jan 6, 2023

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?

@markgravity
Copy link
Contributor

@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))

@blyscuit
Copy link
Collaborator Author

@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
Copy link
Contributor

markgravity commented May 16, 2023

@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?

@blyscuit From my recent experiment, repeating type ViewId then () is so painful so that is why I wrote these functions 🤣

@blyscuit
Copy link
Collaborator Author

@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)))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants