DEV Community

Cover image for Cypress and Page Object Pattern - Good practice for EndToEnd testing
Fabio
Fabio

Posted on • Edited on

Cypress and Page Object Pattern - Good practice for EndToEnd testing

In this article, I will show you how you can adapt the Page Object Pattern in conjunction with a naming convention to save you a ton of time while writing your tests. First we start off with a naming convention and then how we can use Page Object Pattern to our advantage.

Have a look at the end result of the tests.

Be consistent with the naming of your Attribute

This is pretty much a no-brainer, but becomes very important the bigger the project is. If you utilize your naming properly, you will be able to abstract your tests in a very neat way. For instance, you can build selectors based on your component names and actions. What I often do is:

<button class="registration-button"
data-test="registration-page__button-start">Register</button>
Enter fullscreen mode Exit fullscreen mode

or

<button class="open-settings"
data-test="settings-page__button-open">Settings</button>

Enter fullscreen mode Exit fullscreen mode

so I follow these conventions all the time, depending on the project:

// it is just important that you are consistent with it and have a system in place
<page-name>__<element-type>-<semantic-action>
or even
<page-name>-<component-name>__<element-type>-<semantic-action>
or just
<component-name>__<element-type>-<semantic-action>

Enter fullscreen mode Exit fullscreen mode

This helps me to unify selectors and I do not even need to look 👀 at my markup anymore. The first example button is starting the registration process, hence start, the second button is opening a widget/components, hence open. I would recommend that you take the time and define your semantic action and properly communicate them with your team. Good testing starts with a solid naming convention.

If you do this you can save so much time, have a look at the next section where I show how you can utilize this even more.

Split up the test into a reusable Structure

So, with everything in coding once you start to copy your code around you might better off writing a function (I mean fair enough if you just copy one logic from one place to another, and you are sure you will not need it more often than that.) Splitting up your code into semantic logical chunks makes a lot of sense, for maintainability. And if you are going to spend your time writing end-to-end tests, you will spend 10x more on it if you have a huge project, which does not share any logic.

There are two main patterns I have come across and which make more or less sense for certain apps. One is the Page Object Pattern and the other is the Function Object Pattern (at least I call it like this 😅). However, in this article I just show the Page Object Pattern, let me know if you are interested in a more function based approach. 😎

Page Object Pattern

Okay, so the Page Object Pattern is a way of semantically grouping page elements into classes. It is often frowned up on, because it can add a layer of complexity to a smaller project. However, classes do have their space, espacially if the project gets big. Let me show you how I utilize classes in a way to save you a ton of time and improve readability.

Let's say we have two components, which represent both a form, both can be opened and both have submitted buttons and error messages. So, without having seen the component, we already can picture what the test will need to do:

  1. Open the Component (if it is closed) or simply scroll to it
  2. fill out the required input with false data
  3. submit and evaluate if error message(s) are displayed
  4. fill out with correct data
  5. submit and evaluate the success message(s)

In fact, pretty much all form components do this! So we can abstract all of it into a base class like this:

/**
 * The Widget Test Interaction Page Object - abstract class!!
 */
class FormPageObject {
  defaultOpenSelector = '';
  defaultSubmitUrl = '';
  defaultSubmitButtonSelector = '';
  defaultInterceptName = '';
  defaultSelectorForStringInputChange = '';
  defaultCloseSelector = '';
  defaultMessageSelector = '';

  /**
   * Opens the widget
   */
  open(
    selector = this.defaultOpenSelector,
    options = {},
    inputSelector = ''
  ) {
    return cy
      .$(selector, options, inputSelector)
      .click({
        force: true,
      })
      .scrollIntoView({ offset: { top: -100, left: 0 } });
  }


  /**
   * Close the  widget
   */
  close(
    selector = this.defaultCloseSelector,
    options = {},
    inputSelector = ''
  ) {
    return cy.$(selector, options, inputSelector).click({
      force: true,
    });
  }


  /**
   * The intercept will register with the default name and url and will look for the next request
   * hence you can wait for it. By default it is expecting that the response will work.
   * You can also force a success true
   */
  interceptSubmit(
    forceSuccess = false,
    forceFailure = false,
    shouldSucceed = false,
    inputUrl = '',
    interceptName = this.defaultInterceptName,
  ) {
    const url = inputUrl ?? this.defaultSubmitUrl;
    cy.intercept('POST', url, (req) => {
      req.continue((res) => {
        if (forceSuccess) {
          res.body.success = true;
        } else if (forceFailure) {
          res.body.success = false;
        } else if (!res.body.success && shouldSucceed) {
          expect(
            res.body.success,
            'It was not possible to change the the widget and it was expected that it would be possible'
          ).to.be.true;
        }
        return res;
      });
    }).as(interceptName);
  }


  updateStringInput(
    input = '',
    selector = this.defaultSelectorForStringInputChange
  ) {
    let newString = '';
    if (input === '') {
      return cy.stringInput(selector, input);
  }



  submit(
    selector = this.defaultSubmitButtonSelector,
    options = {},
    inputSelector = ''
  ) {
    cy.$(selector, options, inputSelector).click({
      force: true,
    });
  }



  submitValidator(req, messageSelector = this.defaultMessageSelector) {
    cy.isRequestValid(req);
    const correctClass = req.response.body.success
      ? 'alert-success'
      : 'alert-danger';
    cy.checkForClass(
      correctClass,
      messageSelector,
      `The submit request returned ${req.response.body.success}, but the message had not this class ${correctClass}`
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

I abstracted the code and used some of my own helper methods like cy.$ or cy.stringInput. (If you are interested how I build my helpers methods let me know in the comments) The good thing about this abstract class is that now we can use it like this:

class FormComponentA extends FormPageObject {
  defaultOpenSelector = 'form-componentA__button--open';
  defaultSubmitUrl = /regexForSubmitUrl/;
  defaultSubmitButtonSelector = 'form-componentA__button--submit';
  defaultInterceptName = 'formComponentASubmit';
  defaultSelectorForStringInputChange = 'form-componentA__input-text--type';
  defaultMessageSelector = 'form-componentA__message'
}

class FormComponentB extends FormPageObject {
  defaultOpenSelector = 'form-componentB__button--open';
  defaultSubmitUrl = /regexForSubmitUrl/;
  defaultSubmitButtonSelector = 'form-componentB__button--submit';
  defaultInterceptName = 'formComponentBSubmit';
  defaultSelectorForStringInputChange = 'form-componentB__input-text--type';
  defaultMessageSelector = 'form-componentB__message'

  // this component needs some custom interaction, e.g. a slider
  useSlider(percentage = 0, selector = 'default-selector-for-slider') {
    cy.moveSlider(selector, percentage);
  }
}

Enter fullscreen mode Exit fullscreen mode

JumpToNamingConvention

Now we can write the actual test logic like this :


describe('test-for-component-A', () => {
  it('should fail and display error message', () => {
    const component = new FormComponentA();
    component.open();
   
    // e.g. update name input
    component.updateStringInput('Max');
  
    // set up the interceptor and force it to fail
    component.interceptSubmit(false, true);
  
    // hit the submit button
    component.submit();
   
    // wait for the request and evaluate the response
    cy.wait(component.defaultInterceptName).then((req) => {
      component.submitValidator(req);
    });
  });



  it('should succeed and display success message', () => {
    const component = new FormComponentA();
    component.open();

    // set up the interceptor and force it to succeed
    component.interceptSubmit(true);

    component.updateStringInput('Max');
    component.submit();
    cy.wait(component.defaultInterceptName).then((req) => {
      component.submitValidator(req);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Our tests are now very good readable 😇. I think anyone can now understand what the test is actually doing. Readable code is the first step of creating maintainable code! The cool thing is, if we want to create a test for FormComponentB we can very quickly do so!

Coming back to my earlier statement

You should use a solid naming convention for your tests attributes can really help you utilize the power of this approach.

Because you can also do stuff like this in your base class(es):

  constructor(componentName, interceptRegex) {
    this.defaultOpenSelector = `.${componentName}__button-open`;
    this.defaultSubmitUrl = interceptRegex;
    this.defaultSubmitButtonSelector = `.${componentName}__button--submit`;
    this.defaultInterceptName = `${componentName}Intercept`;
    this.defaultSelectorForStringInputChange = `.${componentName}__input-text--type`;
    this.defaultMessageSelector = `.${componentName}-message`;
  }
Enter fullscreen mode Exit fullscreen mode

So now you do not even need to write a new class and can use the abstract FormClass directly. 🔥🔥🔥

const component = new FormPageObject('form-componentB');
Enter fullscreen mode Exit fullscreen mode

JumpToTop.

LET ME KNOW 🚀🚀🚀

  • Do you need help, with anything written above?
  • What will be your first Image? 😄
  • Do you think I can improve - then let me know
  • Did you like the article? 🔥

Top comments (0)