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:
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:
- Click on the search box.
- Enter the text.
- 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:
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
, andprod
. - 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
, orprod
property based on theTESTENV
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.