8

When developing a page object model, the whole intention is generally to abstract the page into methods that keeps code reusable and increase readability.

Something I often get stuck on how high of a level do I abstract something? So for example something like a login form (or really most forms for example).

I generally will code out something such as (This is in Cypress fwiw, but pretend it's in whatever automation framework of your choice. Code below is JavaScript):

  setEmail(value) {
    const field = cy.get('.form-control[name="username"]');
    field.clear();
    field.type(value);

    return this;
  }

  setPassword(value) {
    const field = cy.get('.form-control[name="password"]');
    field.clear();
    field.type(value);

    return this;
  }

Generally at this point is where I wonder "Hmmm should I go ahead and wrap this forms n inputs into a larger method such as":

 login(email, password) {
    this.setEmail(email);
    this.setPassword(password);
    this.submit();
  }

Which at that point I question: "Why even bother writing the independent input methods".

So how do I decide when it's worth writing a large method that encompasses multiple other private methods...and if I write a larger "public" method should I even bother writing smaller private methods?

I read somewhere with PoM that we should:

Model user behaviour not user interfaces

When I think about that statement, especially when it comes to forms it feels like one "larger" method makes sense. The only real advantage I see to splitting them up is in case I need to test negative cases (Such as not all inputs are filled out for instance). Which can technically be conditionally added to a larger form (But can also be a pain when there are multiple inputs).

What is the best practice here?

4

As with any piece of software, you can consider a few heuristics when deciding abstractions:

1 - On a higher-level abstraction, Single Responsibility Principle:

A component (method, class, module) should serve a single actor.

Meaning that a component should change for a single reason.

E.g., if you have a HomePage object that has loginand goToAboutPage, you probably have a problem (unless it's just a Fa├žade). Why? Because by being on the same component, changes on the way you perform login may affect how you perform goToAboutPage.

You solve it by moving these two responsibilities to two different components.

2 - Curly's Law: (A function should) Do one thing

A function should do a single thing, and its name should clearly state it. It avoids adding unintended consequences and complicated bugs complexity of doing more than one thing.

enter image description here

How do you achieve it? Extract till You Drop:

If you can extract part of a function until another, your function does more than one thing.

The login example is perfect:

The following clearly states how you login:

login(email, password) {
    this.setEmail(email);
    this.setPassword(password);
    this.submit();
}

The following clearly states how you set the email:

setEmail(value) {
    const field = getEmailInput();
    field.clear();
    field.type(value);

    return this;
  }

The following clearly states how you find the email input:

  setEmail(value) {
    return cy.get('.form-control[name="username"]')
  }

You could further simply the input set by creating an abstraction for the clear + type behavior.

Your code will read like prose, which is good for your readers (other programmers and your future self). And also, bugs will be very easy to find because one can only put so many bugs in 3-4 lines of code function.

| improve this answer | |
  • I can't quite tell. Are you saying how I have my login function/methods setup IS correct or isn't? – Mercfh Feb 11 at 5:38
  • 1
    There is no correct / incorrect here. As I said, the tips I mentioned are heuristics, fallible guidelines to achieve a result. If your methods are working, try to applying these ideas in order to improve. If you can't apply them so that things get better, you are done. Maybe some other day you will see how to improve - don't get stuck in asymptotic perfectionism. – João Farias Feb 11 at 6:21
6

I wouldn't say the PoM pattern is intended to model a behavior. Rather a user interface. Tests are intended to model behavior.

If you have your object less abstract then you can assert the results more precisely. Having just a login method wouldn't let you assert the things which happens inside login. We'll, technically you'll be able to but asserting anything within your page class is really bad practice.

So I would decouple behavior to a separate class.

| improve this answer | |
  • This is the point I keep going back to. I mean it does help when I have VERY large forms for example (or complex ones) just to prevent the test scripts being bloated...but when it's only a few inputs I see very little benefit. That being said...even with large forms I still need to do other assertions. – Mercfh Feb 10 at 2:22
  • Also what exactly do you mean by "Decouple behavior to a separate class"? As in a Non-PoM class or? (Do you mind giving an example?) – Mercfh Feb 10 at 5:09
  • By "decoupling in a separate class" I mean having some piece of code that is not directly related to your page object (neither a part of a page object not a part of any derivative) but that is acting upon your page object as a "business wrapper". A good example might be a "step definition" in cumber framework. – Alexey R. Feb 10 at 12:44
3

There aren't any hard rules when it comes to writing Page Object Model or any test Framework. The key is to identify the approach that makes it reusable, easy to understand , and less maintenance effort.

Some of the procedures i follow are:

Should i bother writing a wrapper function like login?

  1. Why to bother having login function if it is not used in more than 1 test case.More function calls will actually slows down your program so if its not reused , then better use it as it is.

  2. If it is used at more than 1 place then go with wrapping it with a wrapper function like login. This will reduce the number of code lines and promotes code re usability

Should i have individual methods ?

  1. This is a tricky part, as sendKeys() , click(), clear() are already methods and why you need to wrap it again ? In your case this makes sense as you are not just sending the value but first clearing it then sending it. But if its just send or click then having something like below won't be advisable:

    setEmail(value) {
        field.type(value);
    }
    
  2. But when you see from readability and maintainability perspective, following a common pattern in testcase wouldbe much easy for instance:

    page1.goto()
    page1.click('loginButton')
    page1.sendValue('passwordField','test')
    

looks better than

 let a= page1.goto();
 a.click(loginButton)
 a.sendkeys('test')
| improve this answer | |
  • 1
    Why to bother having login function if it is not used in more than 1 test case. Because it makes your test case much more understandable and maintainable to say doLogin rather than have a mess of function calls. The overhead of a function call is entirely meaningless when dealing with Selenium. – chrylis -cautiouslyoptimistic- Feb 10 at 4:57
  • "more function calls will actually slows down your program", does it really? softwareengineering.stackexchange.com/questions/80084/… – Niels van Reijmersdal Feb 10 at 11:00
  • I like pageObjects to hide all locators and framework usage. So that if we change test-tool we only have to change pageObjects and not the tests. Your page1 examples counters the login() method. I would prefer pageObjects to have methods like login(). – Niels van Reijmersdal Feb 10 at 11:05
3

I like to take it one step further and also make abstractions for test-data like a user.

 login(user) {
    this.setEmail(user.email);
    this.setPassword(user.password);
    this.submit();
 }

This userObject could check if it exists, setup itself. I dont like to hardcode things like passwords in the test.

Follow clean code princibles when splitting larger methods into smaller methods.

| improve this answer | |
1

It is totally depends on individual how he is writing framework. For me, I recommend to break a big ui task in to small steps as well and covert them in small function. As

these small function can be reused/call whenever required and play a vital role in negative test cases and it would be play a good role If you need to test a feature so you can wrap all them as per your need and convert in a big function for code reusability and to accomplish a task.

I will take an example of login from question. You can simply convert it small function.

setEmail(value) {
    const field = cy.get('.form-control[name="username"]');
    field.clear();
    field.type(value);

    return this;
  }

  setPassword(value) {
    const field = cy.get('.form-control[name="password"]');
    field.clear();
    field.type(value);

    return this;
  }

Positive Test Case - Test login functionality (Here you can combine all setEmail and setPassword function and make a login function to test login feature )

login(email, password) {
    this.setEmail(email);
    this.setPassword(password);
    this.submit();
    // Apply assertion on home page
  }

Negative Test Case- Test Login functionality with invalid email and password (here you can call small function individually)

this.setEmail(incorrectEmail);
    this.setPassword(incorrect-password);
    this.submit();
// apply assertion on error message

Negative Test Case- Test Login functionality with invalid email (here you can call small function individually)

this.setEmail(incorrectEmail);
    this.setPassword(password);
    this.submit();
// apply assertion on error message

Negative Test Case- Test Login functionality with invalid password (here you can call small function individually)

this.setEmail(Email);
    this.setPassword(incorrectPassword);
    this.submit();
// apply assertion on error message

Negative Test Case- Test Login functionality with empty email and password (here you can call small function individually)

this.submit();
// apply assertion on error message

Conclusion: small function means we have more flexibility so we can use them as per our need. In above example although we can use login() function with incorrect and empty credentials but in most cases small function can call individually base on condition and test case gives more flexibility

| improve this answer | |
  • 1
    This is what I have been doing so far. For "Happy Path" I am bundling up the functions into one larger "AddWidget" or "Login" function. But I also am spending the time to add the extra smaller functions when I need to test negative test cases (As attempting to ALSO incorporate negative test cases into larger functions would be difficult). – Mercfh Feb 10 at 4:46
1

Page object without good abstract layer is trash which breaking a lot of programing rules like single responsibility and etc. Better to develop an abstract layer with components and then reuse this component in your page classes. You can create shared components and specific components, this similar approach in web development, for example, when you using React JS for development is the common approach.

| improve this answer | |

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