ios-templates icon indicating copy to clipboard operation
ios-templates copied to clipboard

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

Open blyscuit opened this issue 2 years ago • 15 comments

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 avatar Dec 01 '22 09:12 blyscuit

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

suho avatar Dec 01 '22 09:12 suho

Vote on adding UITest ViewId

Vote on KIF

blyscuit avatar Dec 01 '22 09:12 blyscuit

@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 avatar Dec 01 '22 09:12 blyscuit

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

blyscuit avatar Dec 19 '22 08:12 blyscuit

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

suho avatar Dec 19 '22 10:12 suho

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

blyscuit avatar Dec 19 '22 11:12 blyscuit

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

suho avatar Dec 19 '22 11:12 suho

@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 avatar Dec 20 '22 09:12 vnntsu

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

blyscuit avatar Dec 20 '22 09:12 blyscuit

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

blyscuit avatar Dec 21 '22 08:12 blyscuit

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?

nmint8m avatar Jan 06 '23 07:01 nmint8m

@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 avatar May 16 '23 07:05 markgravity

@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 avatar May 16 '23 08:05 blyscuit

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

markgravity avatar May 16 '23 08:05 markgravity

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

blyscuit avatar May 16 '23 08:05 blyscuit