5

Background

So I am sure this will probably get a lot of opinions but I am sure there are some things people with they thought of when starting to implement an automation framework from scratch, which is exactly what I am about to do.

I was given the go-ahead to start as the lead on implementing "automation" (as in we have no automated tests period) on a fairly mature project (Which is good, less maintenance nightmares) but I wanted to start off on the right foot.

I've essentially chosen which tools to use (Enzyme for Component/Unit tests, Cypress for e2e tests and "something" for API/Integration...haven't quite figured that one out). That being said I did work on a "proof of concept" when learning Cypress on this project so I at least have some idea of where to start out there.

On my proof of concept it was also my first dive into the Page Object Model. I quickly became almost stalled when trying to figure out things such as how to structure pages....and how "abstract" do I want to make components. Even things such as how to structure all the page objects and components quickly got me overwhelmed.

Is there a good guide? (Although I guess it's probably opinionated) or things people with they thought of when starting out with PoM. I've also heard of the screenplay pattern but am not super familiar with that either.

Thoughts

Generally (at least in my head) I tried to think about each "Page" as a actual page of the web-application. And tried to abstract individual items (Text boxes/buttons/input fields/etc...) into components. I treated the Page Object as a public api essentially to be called by the test cases (Sort of a black box) but I quickly realized I was doing a lot of duplication in test cases.

For example something such as a Login page, you have a few options here.

  1. Break down the elements (buttons/inputs) into components. Those components have methods (get the element, fill in the element, other special cases as needed)--->Import these components into the "LoginPage" object. call the components individually in the test case.

  2. Just keep all the components in the page object as private selectors (or private methods) and do the same thing as #1.

  3. Do either 1 or 2 but also have public methods as shortcuts in the page object (Such as a "Login()" method that handles everything for you.

I had difficulty when deciding how far down to abstract things and when to make "helper" public methods in the page objects. Since 1 and 2 keeps things abstracted...but you end up making duplicate calls when doing multiple test cases (As opposed to just calling the Login() method).

Sorry for a bit of a ramble, just trying to start out on the right foot.

Problem

How should I handle building this automation from scratch? I definitely want to use the Page Object Model, but I don't want to have too much duplicated code.

3
  • How about keeping an object repository(a xml or json hierarchy page wise to capture UI objects) and collection of simple utility common functions as action library? NO page object model. Commented Jun 12, 2021 at 22:24
  • And just plain test scripts to interact with UI objects with these action functions.What would you miss in that structure without POM? Commented Jun 12, 2021 at 22:27
  • Why do we need classes(POM) when there is no data to hide or state to maintain of DATA which is essentially stateless static UI objects? Commented Jun 12, 2021 at 22:32

4 Answers 4

6

As a general principle, I'd follow a relatively simple approach:

  • YAGNI (You ain't gonna need it) - don't code something until you need to automate it.
  • DRY (Don't repeat yourself) - if you find yourself repeating code, extract the repetitive code into a helper method and call it with the appropriate parameters.

Generally speaking, when I'm starting a new project, especially if it doesn't already have automation in place, I'll start with the simplest test that gives me something useful. Usually it's a login.

Since in the software I work with, nothing else can happen unless the user is logged in, my second test will require the user to be logged in, so logging in will quickly get extracted into a convenience method, which will usually wind up being called as part of the standard test setup code.

After that, I typically define page objects based on the Single Responsibility principle. The page is there to do something, and any and all functions that page handles are in the object. I'll also have a set of common functions that are used across multiple pages that live in separate objects.

The goal I aim towards is to keep the test code as clean and human-friendly as possible. That means that I'll have test code with methods like (pseudocode):

Order_For_Existing_Customer_Generates_Invoice()
    Login_As_Admin(credentials object)
    Go_To_Order_Entry()
    Enter_Order_For_Existing_Customer(order object)
    Verify_Invoice_Has_Correct_Information(order object)

It means I have a collection of sometimes quite complex test data objects, but with complex software this happens. The important part is that the test methods are clear about what is happening - it makes maintenance a lot easier.

4
  • 1
    DRY is sometimes conflicting with DAMP though testing.googleblog.com/2019/12/…
    – Rsf
    Commented Jun 10, 2021 at 12:45
  • @Rsf - Of course. Precisely how DRY test code needs to be depends on the application and the needs of the test team.
    – Kate Paulk
    Commented Jun 11, 2021 at 18:38
  • It sounds like you would probably take complex forms and make them a standalone function/method then in the Page object? Because thats where I always get lost. It feels wrong.....but at the same time adding 10 different methods in the test script isn't really making things "cleaner".
    – Mercfh
    Commented Jun 13, 2021 at 3:12
  • @Mercfh - I tend to do exactly that, although the nature of the application also impacts what I do. For instance, if a customer edit module can be found on 4 different pages, I have a customer edit Page object, with something like editCustomerData() as a function/method. There will be calls to other methods within that method - I typically have convenience methods to handle different field types because I build the Selenium wait.until() calls into the convenience methods.
    – Kate Paulk
    Commented Jun 14, 2021 at 11:29
3

I don't have one advice fits all kind of answer (which is fairly impossible anyway), but just a few observations based on your question:

  • You seem to complicate things too much, focus on actually checking the application, instead of on how and if you should create properties private and have getters and setters
  • I think one of the benefit of JS for test automation is that you don't mess up with private and public properties and methods, since JS historically didn't have support for that. This sounds to me like Java kind of talk where you'd busy yourself with such things. Again, your goal is to actually check something with the tool, not play around with access modifiers.
  • Cypress team has their own opinion about POM https://www.cypress.io/blog/2019/01/03/stop-using-page-objects-and-start-using-app-actions/ I think the bottom line of that article is "don't compliate things by creating another model of the appliaction (which you'll get wrong anyway at some point), and start actually doing the checking".
  • Cypress offers custom commands if you need to reuse some actions in the add
  • If you want to do POM design pattern, you don't need to think that one page class == one actuall page in the app. Perhaps the page object model is not the best name, but you can have a class for only some form on the login page. Again, it depends on what your testing goal is. If you want to check the form, create a POM class for the form only and ignore the rest on the page.
  • The benefit of POM is you have all selectors nicely on one place so if some change, you change them at that one place. But you also can have custom data-testid attributes in the app, so then the selectors never change anyway, so if you use cy.get('[data-testid=abc]'); (or ideally use cy.getByTestId('abc'); and use Testing Library) on two places in the code becomes no problem at all.

These are just mine highly subjective observations based on mostly my own mistakes when I focused too much on being a programmer and playing around with POM, instead of on creating powerful checks.

1

For starters, it's good to remember that

There are good practices in context, but there are no best practices

But I've recently written a blog post on a coding kata for the famous Login Page, showing an approach that may help you to create easier to maintain page object structure. You check it out step-by-step of the refactoring process here. If you want just to checkout the whole code, you can see it here.

What follows are the most important parts of the final version, but the thought process of refactoring will show the justification for the final situation.

1 - The test:

    @Test
    fun performLoginTest() {
        val loginPage = LoginPage.accessPage(driver)

        val secureAreaPage = loginPage.login(makeUserCredentials())

        verifyLoggedPageElements(
            secureAreaPage = secureAreaPage,
            title = "Secure Area",
            subtitle = "Welcome to the Secure Area. When you are done click logout below."
        )
    }

As you can see, the test follows the famous Given-When-Then structure, with no mentioning of web elements or web driver. Actually, things could be even more abstract and not mention "Page" per se, but "Area" or something less connected to the web. This structure and naming makes things easier read and maintain.

2 - The LoginPage Page Object

class LoginPage(driver: WebDriver): Page(driver) {

    private var loginForm: LoginForm

    val flashBannerText: String
        get() = flashBanner.text

    private val flashBanner
        get() = baseLocator.findElement(By.id("flash"))

    init {
        initPage()
        loginForm = LoginForm(baseLocator)
    }

    fun login(userCredentials: UserCredentials): SecureAreaPage {
        loginForm.login(userCredentials)
        return SecureAreaPage(driver)
    }

    companion object {
        fun accessPage(driver: WebDriver): LoginPage {
            driver.get("https://the-internet.herokuapp.com/login")
            return LoginPage(driver)
        }
    }
}

Two important decisions here can be highlighted:

1 - The choice to create an LoginForm object:

The username, password, and loginButton fields of a typical login page have a connected purpose of being filled with the credential data and clicking on the login button. And if you look at the DOM, they are organized inside a div, a sign they are to act together. Thus, you can encapsulate them inside an object and provide only the behavior: LoginForm::login(credentials)

2 - The flashBannerText is an entity in the highest level of the Login page. Thus, it will stay at the same level. We could have created an object to encapsulate the two functions too.

Here is the The LoginForm component:

class LoginForm(baseLocator: WebElement) {
    private var baseLocator = baseLocator.findElement(By.id("login"))

    private val usernameField: WebElement
        get() = baseLocator.findElement(By.id("username"))

    private val passwordField: WebElement
        get() = baseLocator.findElement(By.id("password"))

    private val loginButton: WebElement
        get() = baseLocator.findElement(By.tagName("button"))

    fun login(userCredentials: UserCredentials) {
        fillFields(userCredentials)
        performLogin()
    }

    private fun fillFields(userCredentials: UserCredentials) {
        fillUsernameField(userCredentials.username)
        fillPasswordField(userCredentials.password)
    }

    private fun fillUsernameField(username: String) {
        usernameField.sendKeys(username)
    }

    private fun fillPasswordField(password: String) {
        passwordField.sendKeys(password)
    }

    private fun performLogin() {
        loginButton.click()
    }
}

As explained in the blog post, this structure emerges when you follow Extract Til You Drop and apply encapsulation. If you are worried about too small objects or functions, I would remember that we spend more time reading code than writting - you write something once, but you and multiple people will read it many times. With small functions you give the option to your reader to choose if he/she wants to read the details or not.

For instance, one can quickly read this and be satisfied in knowing how the login works.

fun login(userCredentials: UserCredentials) {
            fillFields(userCredentials)
            performLogin()
}

However, with a bigger function, one has to read way more, and in a less abstract language, to know what is happening on the login:

fun login(userCredentials: UserCredentials) {
            usernameField.sendKeys(username)
            passwordField.sendKeys(password)
            loginButton.click()
}

The same goes for objects/classes. If you name things well and break them down into ever smaller pieces, you give the option to your reader to pick the level of detail he/she wants to know.

In summary, generally speaking, a good practice is to map the DOM tree structure to the tree structure of your components (that represents pages, forms, etc). When the DOM changes, you would have to make a similar change.

Exceptions apply, of course, but it is generally due to the confusing structure of the DOM created by bad use of frontend frameworks. Advocating for checkability is important.

0

I had difficulty when deciding how far down to abstract things and when to make "helper" public methods in the page objects.

Yup, the balance is hard to achieve.

One approach I use is to consider 'levels of abstraction' as a guide.

For instance if I have code that completes a form... that uses code that logs in... that uses a method to extract the username... that uses... etc

When, every line you look at requires you to dive up and down into multiple levels of code this creates cognitive overload on short term memory and makes the work harder.

I try and keep the abstractions to 2 or 3 levels. This means cxonstantly balancing 'depth' with 'width'. Depth being the number of levels of abstraction. This means that sometimes a little duplication and use of hard coded text values is better than abstraction which will require you to go and look at the abstraction.

There are many approaches to use depending on the situation, such as

  • extracting to a method within the existing code method, class, etc.
  • extracting to an external method/class/interface/helper
  • using page objects and data objects to name identifiers and test data
  • using tags to organize cases
  • using literal text instead of extracting and maintaining test data

The result can be as follows:

enter image description here

For page objects I would consider having the following

  • application-wide identifiers such as login, admin, etc.
  • workflow specific identifiers that are shared between various scenarios
  • page level identifiers specific to a process
  • component level identifiers if a page uses the component multiple times
1
  • This is an old comment, but what exactly does p represent here?
    – Mercfh
    Commented May 16 at 18:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.