Book Image

UI Testing with Puppeteer

By : Dario Kondratiuk
Book Image

UI Testing with Puppeteer

By: Dario Kondratiuk

Overview of this book

Puppeteer is an open source web automation library created by Google to perform tasks such as end-to-end testing, performance monitoring, and task automation with ease. Using real-world use cases, this book will take you on a pragmatic journey, helping you to learn Puppeteer and implement best practices to take your automation code to the next level! Starting with an introduction to headless browsers, this book will take you through the foundations of browser automation, showing you how far you can get using Puppeteer to automate Google Chrome and Mozilla Firefox. You’ll then learn the basics of end-to-end testing and understand how to create reliable tests. You’ll also get to grips with finding elements using CSS selectors and XPath expressions. As you progress through the chapters, the focus shifts to more advanced browser automation topics such as executing JavaScript code inside the browser. You’ll learn various use cases of Puppeteer, such as mobile devices or network speed testing, gauging your site’s performance, and using Puppeteer as a web scraping tool. By the end of this UI testing book, you’ll have learned how to make the most of Puppeteer’s API and be able to apply it in your real-world projects.
Table of Contents (12 chapters)

Organizing our code

Our first test was quite simple: we were just checking the page title. But let's take a look at the home page:

Packtpub home page

Packtpub home page

There are many actions we would like to test there:

  • Search for an existing book.
  • Search for a non-existing book.
  • Check the cart when it is empty.
  • Check the cart when we add a product.

Let's take, for example, Search tests. We would be doing the same steps every time:

  1. Click on the search box.
  2. Enter the text.
  3. Click on the search button.

We would be doing the same thing over and over in all our search tests. Sometimes there is a misconception that, as the test code is not production code, the code can be a mess. So, people go and copy/paste their tests over and over, duplicating code and hardcoding values. That ends up with hard-to-maintain tests. When tests are hard to maintain, they tend to be pushed down the priority list. Developers lose, QA analysts lose, and in the end, clients lose.

We are going to see two techniques to improve our test code: the Page Object Model (POM) and the test data config.

Introducing the Page Object Model

The POM is a design pattern that will help us separate our test code from the implementation of the interaction our tests will perform.

Let's build our HomePageModel together. What are the possible interactions on that page?

  • Go (to the page)
  • Get page title
  • Search
  • Sign In
  • View Cart
  • Go to Checkout
  • Subscribe

Well done! We just created our first Page Model. This is how it will look:

module.exports = class HomePageModel {
    go() {}
    title() {}
    search(searchValue) {}
    signIn() {}
    viewCart(){}
    gotoCheckout(){}
    subscribe(){}
}

Let's focus on the two first functions: the go function, which will navigate to the home page, and the title function, which will return the page title.

We will reuse a lot of code here. If we want to start using this model, we would need to do two things: implement the title fetching here and pass a Puppeteer page to this model:

export default class HomePageModel {
    constructor(page) {
        this.page = page;
    }
    // Unused functions…
    async go() {
        await this.page.goto('https://www.packtpub.com/');
    }
    async title() {
        return await this.page.title();
    }
}

Now it's a matter of importing this class into our tests using require. I will put this class into a POM (Page Object Model) folder inside the test folder. Once we create the file, we import it:

const HomePageModel = require('./pom/HomePageModel.js');

We declare a variable inside the describe:

let homePageModel;

We create an instance of this class in the beforeEach hook:

beforeEach(async () => {
    page = await browser.newPage();
    homePageModel = new HomePageModel(page);
    await homePageModel.go();
});

And now, we simply replace the page.title we are using with homePageModel.title:

(await homePageModel.title()).should.contain('Packt');

As I mentioned earlier in the chapter, UI tests help us see whether our refactoring broke our code. Let's run npm test again to confirm that we didn't break anything:

Test result after the first refactor

Test result after the first refactor

There's only one thing left to do so that we can be proud of our first project. We need to get rid of our hardcoded values. We only wrote two tests, and we have three hardcoded values: the site URL and the Packt and the Books words.

For these tests, we can leave these hardcoded values. But what if you have different environments? You would need to make the URL dynamic. What if your site were a generic e-commerce site? The brand name would depend on the test you are navigating.

There are many other use cases:

  • Test users and passwords
  • Product to test
  • Keywords to use

We can create a config.js file with all the environment settings and return only the one we get on an environment variable. If not set, we return the local version:

module.exports = ({
    local: {
        baseURL: 'https://www.packtpub.com/',
        brandName: 'Packt',
        mainProductName: 'Books'
    },
    test: {},
    prod: {},
})[process.env.TESTENV || 'local']

If this looks a little bit scary, don't worry, it's not that complex:

  • It returns an object with three properties: local, test, and prod.
  • In JavaScript, you can access a property by using object.property or by treating the object as a dictionary: object['local'].
  • process.env allows us to read environment variables. We won't be using environment variables in this book, but I wanted to show you the final solution.
  • Finally, we are going to return only the local, test, or prod property based on the TESTENV variable or 'local' if the environment variable was not set.

I bet that by now, you will know that we will be able to access this object using a require call:

const config = require('./config');

And from there, start using the config variable instead of hardcoded values. We would also need to pass this config to the page model because we have a hardcoded URL there.

After making all these changes, this is what our tests should look like:

const puppeteer = require('puppeteer');
const expect = require('chai').expect;
const should = require('chai').should();
const HomePageModel = require('./pom/HomePageModel.js');
const config = require('./config');
describe('Home page header', () => {
    let browser;
    let page;
    let homePageModel;
    before(async () => browser = await puppeteer.launch());
    beforeEach(async () => {
        page = await browser.newPage();
        homePageModel = new HomePageModel(page, config);
        await homePageModel.go();
    });
    afterEach(() => page.close());
    after(() => browser.close());
    it('Title should have Packt name', async() => {
        (await homePageModel.title()).should.contain(config.brandName);
    });
    it('Title should mention Books', async() => {
        expect(await homePageModel.title()).to.contain(config.mainProductName);
    });
});

If we remove all the unused functions, our final page model would look like this:

module.exports = class HomePageModel {
    constructor(page, config) {
        this.page = page;
        this.config = config;
    }
    async go() {
        await this.page.goto(this.config.baseURL);
    }
    async title() {
        return await this.page.title();
    }
}

As you can see, we didn't need to implement complex design patterns to make our tests reusable and easy to maintain. I think it's time to get started with our tests, which we will do in Chapter 3, Navigating through a website.