Cypress – Using Page Objects

Cypress – Using Page Objects

Powerful JavaScript testing framework

In the first article of this new series, we learnt how to add Cypress to a new project and configure it, finishing off by writing our first test. Congratulations, you’re now at the point where some dishonest people would add it to their CV as a technical skill. However, we have plenty to learn yet, and in this article, we’re going to be improving on our first test by implementing page objects into Cypress.

Cypress, like nearly all test tools/frameworks out there, supports different design patterns, for this series, and to be consistent with the previous series, we’re going to use page objects. This will hopefully help you to see two things. One, how similar the code looks (syntax aside) when using page objects in Cypress compared to Selenium, but also, just how good Cypress is at allowing clear and easy to read code to be written quickly.

We’re going to start from where we left off previously, so hopefully, you still have your solution from last time that you can open in Visual Studio Code. Once it’s open, we’re going to create a new folder in our Cypress root folder, you can either do that by right-clicking on the cypress folder or if you fancy using the terminal:

mkdir cypress\pageobjects

Now we have our ‘pageobjects’ folder, which unsurprisingly, is going to contain our classes for any pages we wish to automate. Some tutorials or examples you might see online will start by creating a base class called Page or something similarly generic, which will contain any and all code that is common across all pages. However, personally, I feel that with Cypress, this isn’t really necessary, and would be adding extra complexity for no real benefit. For now, we will simply create a class for each page we need. Given we’re testing Google, for now, let’s create a HomePage.js file in our ‘pageobjects’ folder.

Obviously the Google homepage isn’t the most complex of pages, so we’re not going to be demonstrating a huge variety of element types (Cypress cheat sheet is coming shortly for that) but this will give you an idea how to organise your page object classes.

export class HomePage {

    searchTextBox= () => cy.get('.Search')
    searchButton= () => cy.get('button').contains('Search')
    feelingLuckyButton= () => cy.get('button').contains('Lucky')
    
    navigate = () => {
        cy.visit("https://www.google.co.uk/")
    }

    performSearch = (searchTerm) => {
        searchTextBox().type(searchTerm)
        searchButton().click()
    }
}

Let’s talk through the code above, and before we get into the Cypress specific code, I want to talk about some of the syntax shown. If you’ve used JavaScript before, or even if you haven’t and just bought some books/looked at online tutorials, you might see some code that looks slightly different to what you’ve previously seen, such as the way functions are written.

The use of arrow functions in the example above is part of ES6. ES6 refers to version 6 of the ECMA Script programming language, and it’s a big improvement to JavaScript with many cool features added. Before this, the code above would have looked something like:

    searchTextBox() = cy.get('.Search')
    searchButton() = cy.get('button').contains('Search')
    feelingLuckyButton() = cy.get('button').contains('Lucky')
    
    navigate() {
        cy.visit("https://www.google.co.uk/")
    }

    performSearch(searchTerm) {
        searchTextBox().type(searchTerm)
        searchButton().click()
    }

Again, because of the lack of complexity in our code, it’s hardly worlds apart, and the differences are subtle but definitely worth mentioning. And the reason they are worth mentioning is that ultimately, both are correct and you can absolutely use both. If you’ve previously learnt JavaScript pre-ES6 and want to write code in that way, please feel free. But it would be beneficial for those who don’t have any experience to begin their learning journey writing ES6 code. In other frameworks/libraries, you’ll have to configure ES6 with Babel and presets, thankfully Cypress does all this for us, so you can just concentrate on writing code.

We went off track a bit there but I felt it was important to mention. Back to the code…

In our Selenium series, when creating Page Objects classes, we would have a class set up in a pretty standard way, a number of properties for our individual elements, and then methods, or actions, for wrapping those locators and doing something with them. In the example above, we just have a list of methods, but we’re using them in two different ways. One set of methods to find and return our individual elements, and like before, another set for wrapping these and performing actions.

Some may read this and think “why not just find the elements in the action method you want to use them in?”, which is a good question. That’s a totally correct way of doing it, but I feel it’s good practise to do it the way I’ve shown. The reason for doing so is that now, we have methods we can use for assertions or methods that can be used in multiple action methods, instead of having to find elements in multiple methods, therefore reducing code duplication. It may make more sense to do it the other way if your application has been designed in a certain way, but that’s a choice for you to make and neither is incorrect.

searchTextBox = () => cy.get('Search')

cy.get(‘Search’) is the equivalent syntax of FindElement in Selenium. By default it takes a Selector locator, there are plugins for Cypress to use things like XPath, but out the box, Selectors are the locators of choice, and that’s no bad thing. Similar to before, you can use dev tools to find the selector you need, or using the Cypress runner, you’ll see a locator playground where you can get the locator (and the code) you need to use, but don’t rely on these as they aren’t the most flexible or reliable of locators, it’s better to get your own.

With the correct locator in cy.get, it will return a single element, and thanks to the cleanliness of ES6 arrow methods, we don’t need to specify a return as by default, single line arrow methods will return the element/object, so we can have a clean, single line of code.

You can see with the buttons, I’ve taken advantage of Cypress’ ability to use contains for finding buttons with certain values or text. Rather than use a complex locator, I’ve instead told it to find a element of the type button, with the text Search or Lucky. Because it’s a contains check, I can use a partial string and it will match. Obviously not best practise in a real application with potential elements having the same term, but for this example, it will do just fine.

navigate = () => {
        cy.visit("https://www.google.co.uk/")
    }

Our next method is the navigate method, which is a pretty standard method to use across all page classes. While we may have already set up the base url in our Cypress config, it’s still good to set up navigate methods for our other urls, for occasions we may want to navigate directly to them without having to rely on the UI. cy.visit will do as the name suggests, visit the page passed to the method in the form of a string parameter.

performSearch = (searchTerm) => {
        searchTextBox().type(searchTerm)
        searchButton().click()
    }

Our second method is our search method, where we will pass a search term in to the text box and let Google do its thing. Here you can see how we are using our element methods to return the object, but also interacting with that returned object. Our first interaction is with passing the search term in to the text box. type will simulate each individual key press as opposed to passing the whole string in in one go, so keep in mind that type may not be the best method to use for long strings (not if test duration is important…).

click() is our second interaction, where we now click the Search button and let Google perform the search with our search term.

We now have our first page objects class finished and ready to use in our tests. You can see in the full example that we are using the export keyword at the start of our class. This will allow us to use our class and instantiate an object of HomePage later on.

Let’s go back to our test file and show you how to use HomePage in our tests:

import { HomePage } from './pageobjects/HomePage'

describe('My First Tests', () => {
    const homePage = new HomePage()

    it('Can succesfully search using a search term', () => {
        homePage.navigate()
        homePage.search('test')
        cy.title().should('contain', 'test');
     })
})

To use our HomePage class, we simply import it (which we’re able to do thanks to our previous export statement in the pageobjects class). This is similar to a ‘using’ statement that we’ve used in our Selenium and C# series, except this time we have to use not only the class/namespace but also point it to the file, including the location.

After that, it’s just a case of creating a new instance of it and assigning it to a variable, in this case a const. We do this in the describe block as it then allows us to use it in any of our tests, or it blocks. If we did it inside the it block, it would only exist for the scope of that test.

Once inside the test, like with our Selenium page objects tests, it’s then just a matter of using that object to call the methods we wish to use for that test. At the end we can use the Cypress assertions to check that our test is passing as expected. If we were to write this same test without the use of page objects, it would be around 9 lines of code, without chaining, and be a lot less readable. We’ve covered the benefits of Page Objects in a number of articles on the site, so I didn’t want to go and make a hugely elaborate page objects class to make a point, as the benefits of page objects translate to any tool or framework, the key here was showing how you could implement it in Cypress and how easy it is.

For the next article in the series, we’re going to move away from Google and automate something a bit more complex, that will mean having to write some commands to add to Cypress as well as using Fixtures to allow us to use test data. While I appreciate automating Google hasn’t been the most exciting thing, it’s important to start with something basic so you can easily follow and wrap your head around the basics.