Reusable page objects

🚧

TDK Feature

This is not supported in Testim's visual editor.

In the Selenium world and in automation testing in general Page Objects is a design pattern that separates the functionality of a specific page into a separate class.

With the Testim Dev Kit. We recommend separating things into page objects logically.

Since Testim coded tests are just JavaScript - you can architecture your code in any way you want but we recommend a simple architecture

Evolving a Simple Architecture

Let's say you are writing tests for a travel website. One of the most common actions you might want to do is log into your website.

It is important to separate the logging in from the application code. This allows you to reuse your log-in code in other tests.

Note: The boilerplate and requires are omitted from this code for brevity but you can run all of these examples in the [http://github.com/testimio/codim-kitchenshink].

You might start with something like the following:

test('a user flow', async () => {
  await go('http://demo.testim.io');
  await type('.login-form input[type=text]', 'Benjamin');
  await type('.login-form input[type=password]', 'correcthorsebatterystaple');
  await submit('.login-form input[type=submit]);
  // test user flow
  await click('.goat');
  const whatGoatThinks = await text('.goat');
  expect(whatGoatThinks).to.equal('Testim Code is fresh!');
});

This is fine, but it is unclear how the code is shared. Moreover, if multiple test flows require logging in one might be tempted to put it in a beforeEach, but that would still make sharing it across tests significantly harder than it has to be.

Composing with functions

Our basic unit of composing code in programming is a function. Functions are great, and indeed since Testim coded tests are just JavaScript, reusing the login step is easy. We start with:

async function login(username = 'Benjamin', password = 'hunter2') {
  await type('.login-form input[type=text]', username);
  await type('.login-form input[type=password]', password);
  await submit('.login-form input[type=submit]);
}

test('a user flow', async () => {
  await go('http://demo.testim.io');
  await login();
  // test user flow
  await click('.goat');
  const whatGoatThinks = await text('.goat');
  expect(whatGoatThinks).to.equal('Testim Code is fresh!');
});

Here, login is separated into a separate function that can be reused between tests and imported across files.

This will let us swap our login implementation into one that logs in through localStorage or a direct API call in the future.

Becoming Classy

As this sort of logic grows, you will want to extract the functionality of the login page as well as other interactions into a separate file. For example, here is account.js

// account.js
module.exports = class Account {
  async login(username, password) {
    // same as before
  }
  async signup(username, password) {
    // record or write a signup logic
  }
  async loggedInUser() {
    // returns a string for the logged in user with 
    return text('.loggedinusername');
  }
  async logout() {
    // ditto for logout
  }
  async restorePassword() {
    // you can use Testim's email generation and validation step for this
  }
}

// goat.test.js

const { test } = require('testim');
const Account = require('./account');
describe('what the user thinks', async () => {
  let account;
  beforeEach(() => {
    account = new Account(); // pass configuration, environment or parameters here
  });
  it('does a user flow', async () => {
    await account.login('Benjamin');
    expect(await accountloggedInUser()).to.equal('Benjamin');
    await account.logout();
  });
});

Note that the nice thing about the code above is that the actual test does not contain any implementation detail. This means that the actual test file is not aware of selectors or locators or what part of the HTML page is interacted with and instead it focuses just on functionality.