Selenium/Explanation/Page object pattern
Code from this page is available at mediawiki/core and gerrit:1066807. |
This page gives more information about page object pattern mentioned at Selenium/Explanation/Stack page.
This tutorial will assume that you are running tests from your machine, targeting MediaWiki-Docker. For more examples see Selenium/Reference/Example Code.
Examples will:
- create an user via the API
- log in as the newly created user using the web interface
- check that the user is logged in
Purpose
editThere are two main purposes of the page object pattern.
- Move parts of the application that change a lot (user interface) to a central location. When the user interface changes, the test suite only needs to be updated in one place.
- Make tests easier to read and understand by moving complexity to page objects.
Let's describe the two purposes with code samples from MediaWiki Core (as of August 2024).
Example
editWith page object pattern
editThe code is split into two files. user.js
contains the test. createaccount.page.js
contains the page object.
Please notice how readable user.js
is. You can immediately see that the point of User should be able to log in
is to log in as a user with credentials username
and password
.
'use strict';
const assert = require( 'assert' );
const UserLoginPage = require( 'wdio-mediawiki/LoginPage' );
const Api = require( 'wdio-mediawiki/Api' );
const Util = require( 'wdio-mediawiki/Util' );
describe( 'User', () => {
let password, username, bot;
before( async () => {
bot = await Api.bot();
} );
beforeEach( async () => {
await browser.deleteAllCookies();
username = Util.getTestString( 'User-' );
password = Util.getTestString();
} );
it( 'should be able to log in', async () => {
// create
await Api.createAccount( bot, username, password );
// log in
await UserLoginPage.login( username, password );
// check
const actualUsername = await browser.execute( () => mw.config.get( 'wgUserName' ) );
assert.strictEqual( await actualUsername, username );
} );
} );
Please notice that LoginPage.js
has two major sections. In the first section are elements on the page (username()
, password()
...). The second section contains actions (open()
and login()
).
tests/selenium/wdio-mediawiki/LoginPage.js
'use strict';
const Page = require( './Page' );
class LoginPage extends Page {
get username() {
return $( '#wpName1' );
}
get password() {
return $( '#wpPassword1' );
}
get loginButton() {
return $( '#wpLoginAttempt' );
}
open() {
super.openTitle( 'Special:UserLogin' );
}
async login( username, password ) {
await this.open();
await this.username.setValue( username );
await this.password.setValue( password );
await this.loginButton.click();
}
}
module.exports = new LoginPage();
Without page object pattern
editLet's compare that to a solution without page object pattern.
tests/selenium/docs/Page_object_pattern/specs/login.js
'use strict';
const Api = require( 'wdio-mediawiki/Api' );
const Util = require( 'wdio-mediawiki/Util' );
// baseUrl is required for our continuous integration.
// If you don't have MW_SERVER and MW_SCRIPT_PATH environment variables set
// you can probably hardcode it to something like this:
// const baseUrl = 'http://localhost:8080/wiki/';
const baseUrl = `${ process.env.MW_SERVER }${ process.env.MW_SCRIPT_PATH }/index.php?title=`;
describe( 'User', () => {
let password, username, bot;
before( async () => {
bot = await Api.bot();
} );
beforeEach( async () => {
username = Util.getTestString( 'User-' );
password = Util.getTestString();
} );
it( 'should be able to log in without page object', async () => {
// create
await Api.createAccount( bot, username, password );
// log in
await browser.url( `${ baseUrl }Special:UserLogin` );
await $( '#wpName1' ).setValue( username );
await $( '#wpPassword1' ).setValue( password );
await $( '#wpLoginAttempt' ).click();
// check
const actualUsername = await browser.execute( () => mw.config.get( 'wgUserName' ) );
expect( actualUsername ).toBe( username );
} );
} );
Conclusion
editWith page object pattern, the framework is more complex, there are more files (two instead of one) and there is more overall code, but the tests are more readable and more maintainable.
Without page object pattern, the framework is simpler, there are less files (one instead of two) and there is less overall code, but the tests are less readable and less maintainable.
To keep this page short, I've selected a simple example. If the test suite is logging the user in a lot, without page object pattern, code for logging in would have to be copy/pasted, leading to duplication and harder maintenance.
With the page object pattern, logging in is just a call to UserLoginPage.login()
function. If any of the elements for logging in change, only LoginPage.js
needs to be updated. If the logic of logging in changes (for example, login button
is now a div
or a span
) only UserLoginPage.login()
needs to be updated.
More information
edit- PageObject by Martin Fowler (2013)
- Page object models by Diego Molina (Selenium, 2019)
- Page Object Pattern by Christian Bromann (WebdriverIO, 2021)
- Selenium/Explanation/Stack
- Selenium/Reference/Stack