We love open source and we invest in continuous learning. We give back our knowledge to the community.

Santiago Ferreira

Using the Page Object Pattern With Ember CLI

Comments

One of the most appealing features in Ember and Ember CLI is the ability to easily create functional or acceptance tests. But, the everyday interaction between UX and development, can hurt how these tests are maintained. Here, I try to describe an approach that helped us overcome this problem.

Let’s consider a simple example. Let’s assume we have a list of users and, we want to validate that a table with 2 users is rendered, so that we can later validate each user’s name.

1
2
3
4
5
6
7
8
9
10
11
12
13
<table>
  <caption>Users list</caption>
  <tbody>
    <tr>
      <td>Jane</td>
      <td>Doe</td>
    </tr>
    <tr>
      <td>John</td>
      <td>Doe</td>
    </tr>
  </tbody>
</table>

Ember, and more specifically ember-testing, provide a DSL that simplifies creation and validation of these conditions on our tests. An example of such an acceptance test in Ember would be:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import Ember from 'ember';
import { module, test } from 'qunit';
import startApp from '../helpers/start-app';

var application;

module('An Integration test', {
  beforeEach: function() {
    application = startApp();
  },
  afterEach: function() {
    Ember.run(application, 'destroy');
  }
});

test('List shows two users', function(assert) {
  assert.expect(4);
  visit('/users');
  andThen(function() {
    assert.equal(find('.users caption').text(), 'Users list');
    assert.equal(find('.users tr').length, 2, 'The list contains two users');
    assert.equal(find('.users tr:first .name').text(), 'Jane');
    assert.equal(find('.users tr:last .name').text(), 'John');
  });
});

As we can see in the above example, after visiting the /users route we validate that the data shown is what we expect.

The problem

While working on a client project, where we generate dozens of acceptance tests, we noticed that many of the CSS selectors used to look up elements were repeated across tests. In some cases, this repetition seemed like a smell.

Then, in some cases the complexity of selectors used prevented us to easily identify what we were trying to test. This can become very confusing, concealing the original purpose for the test. Take for example:

1
assert.equal(find('.users tr:nth-of-type(3) .name'), 'John Doe');

Another issue was how maintainable such tests became. For every change in the HTML, no matter how big or small, we’d probably need to update many tests to make these CSS selectors match.

The solution

Here’s where a widely-used design pattern came to the rescue: Page Objects. The main idea behind this pattern is to encapsulate the page structure being tested with an object, hiding the details of its HTML structure and therefore exposing the semantic structure of the page only.

In our case, the goal was to make the intention of the test clearer, hiding the fact that the users list is an HTML table. We also wanted to make our assertions as obvious and concise as possible, easier to read and understand.

Back to our example, this is a possible implementation for a Page Object:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var usersPage = {
  visit: function() {
    return visit('/users');
  },
  caption: function() {
    return find('.users caption');
  },
  usersCount: function() {
    return find('.users tr').length;
  },
  firstUserName: function() {
    return find('.users tr:first .name').text();
  },
  secondUserName: function() {
    return find('.users tr:first .name').text();
  }
};

Now, we can take advantage of this object and write our acceptance test in an simpler manner:

1
2
3
4
5
6
7
8
9
10
test('List shows two users', function(assert) {
  assert.expect(4);
  usersPage.visit();
  andThen(function() {
    assert.equal(usersPage.caption(), 'List of users');
    assert.equal(usersPage.countUsers(), 'The list contains two users');
    assert.equal(usersPage.firstUserName(), 'Jane');
    assert.equal(usersPage.secondUserName(), 'John');
  });
});

As we can see, the intention of what we want to test is much clearer after applying the Page Objects pattern.

A step further

After applying this process to our application and introducing several objects, we started to notice a few additional patterns emerging. Our team decided then to extract those into auxiliary functions, which made the creation of Page Objects even easier.

We were then motivated to extract this into an Ember CLI add-on named ember-cli-page-object, which provides a small DSL for creating Page Objects in a declarative fashion.

If using the add-on, our previous example translates into:

1
2
3
4
5
6
7
var usersPage = PageObject.build({
  visit:          visitable('/users'),
  caption:        text('.users caption'),
  usersCount:     count('.users tr'),
  firstUserName:  text('.users tr:first .name'),
  secondUserName: text('.users tr:last .name')
});

A login page, for example, can be modeled like:

1
2
3
4
5
6
7
var login = PageObject.build({
  visit:        visitable('/login'),
  userName:     fillable('#username'),
  password:     fillable('#password'),
  submit:       clickable('#login'),
  errorMessage: text('.message')
});

This allows to express test intentions in a cleaner way:

1
2
3
4
5
6
7
8
9
10
11
test('Invalid log in', function(assert) {
  assert.expect(1);
  login
    .visit()
    .userName('user@example.com')
    .password('secret')
    .submit();
  andThen(function() {
    assert.equal(login.errorMessage(), 'Invalid credentials!');
  });
});

You can check out more examples and instructions on how to plug it into your own projects here.

For further information on the Page Object pattern, I recommend reading Martin Fowler’s original description here, as well as the definition on the Selenium’s wiki page here.

Getting involved

Try it out! We’re always looking for ways to improve the project. You can contribute by suggesting new features, fixing bugs, improving the documentation and working on the features from the wish list.

Comments