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

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.

Rails API to Be Part of Rails 5

Comments

A decision was made to incorporate Rails API into Rails core 🎉 🎉 🎉. During the last week I’ve been working on this and, today we opened a pull request to discuss the results.

What is Rails API?

The original idea behind Rails API was to serve as a starting point for a version of Rails better suited for JS-heavy apps. The project consists of: Rails API per se, the Active Model Serializers project plus a bunch of ideas that haven’t been implemented yet. As of today, Rails API provides: trimmed down controllers and middleware stack together with a matching set of generators, all specifically tailored for API type applications.

For more detailed information about the Rails API project, please read my previous article on the subject.

Next steps: What we need to talk about?

We still need to discuss the “Rails way” for API applications, how API apps should be built and, what features we’d like included from our original list of ideas. In particular:

  • Do we want to avoid asset generation in Rails by having a back-end and a front-end apps?
  • Do we prefer to have a single application and keep asset generation in Rails instead?
  • Do we like Active Model Serializers better than Jbuilder?
  • If not, can we make Rails API play nicely with Jbuilder?

Join the conversation

Like every year, I’m attending RailsConf 2015 in Atlanta. This could be a great opportunity to meet and interact. So, please come find me throughout the conference or say hi if we run into each other. I’d love to talk about Rails API or any other topic. Comments, reviews, suggestions and improvements are always welcome.

ActiveModel::Serializers Rewrite (Upcoming 0.9.0.pre Version)

Comments

First of all, I want to apologize to all for the long time it has taken me to push this humble new code.

I started to work on ActiveModel::Serializers because I’m interested in the Rails API project in general and ActiveModel::Serializers in particular. Given that ActiveModel::Serializers has few contributors, I thought it could be a good opportunity to understand the code and help the community around the project.

Rails 4 Links Compilation

Comments

I’m leaving here a curated compilation of interesting links where you will find information that is not very well known. There are pull requests, issues, commits, examples, posts, videos and more about Rails 4.

Rails 4 in 30’

Comments

I gave a presentation in RubyConf Argentina about what’s new in Rails 4 (if you saw it please rate it). I’ve already posted the Rails 4 MindNode which I used to start to think about what I was going to present. The talk was in Spanish but I’m leaving here the English version of the slides.

Rails 4 in a MindNode

Comments

I’ll be talking at RubyConf Argentina, and the first thing I usually do when preparing talks is to think in a high level and then start going down form there. I find MindNode a great tool for that. So I started checking what was being added, removed and deprecated in Rails 4 (my memory isn’t good enough to have all that in the top of my head :P). The result is this MindNode I’m sharing with you …

Ruby Refinements Landed in Trunk

Comments

Refinements arrived to Ruby trunk here. The purpose of Refinements is to make monkey patching safer, extending core classes but limiting its effects to a particular area of code.

Rails for API Applications (Rails-api) Released

Comments

rails-api is a plugin developed by Yehuda Katz, José Valim, Carlos Antonio da Silva and me (Santiago Pastorino) which modifies Rails applications trimming down usually unneeded Rails functionalities for API applications. Do you remember we added support for this on core and it was reverted?. This plugin enables that again.

Bundle Exec Rails … Executes Bundler.setup 3 Times

Comments

TL;DR: don’t run bundle exec before rails command, rails already checks the presence of Bundler through the Gemfile and sets up everything according to it without the overhead of bundle exec.rails command is the only exception to the rule. Additionally I’ve added a patch to Bundler that avoids calling Bundler.setup which adds unnecessary overhead.