View on GitHub

Transition.js 2.0

Detangled in-Browser Webapp Testing

Rationale

Transition.js was created to address specific issues I was having with trying to test my websites on Mobile and Tablet devices.

The websites I needed to test were invariably asynchronous. This lead to writing tests that poll and wait for asynchronous events to complete. Detecting when events completed was error-prone. The tests were brittle and would fail inconsistently due to timing issues.

I wanted a test suite that drove the user interface of the website as close to how a user would. Not just a framework that would test the underlying JavaScript code at the unit level.

I wanted a solution that would execute a test suite on mobile devices (phones and tablets).

At the time I started Transition.js there was no tool or framework that I knew of that fit this criteria.

Inverting the need to Poll

Since waiting for a condition to become true before continuing was a core part of the tests I was writing, I made the idea of waiting core to the testing framework. The most natural way of expressing this for me was to implement tests as a State Machine.

Each test has a name, an optional `initialize` function and a list of states. Each state has a name, an initializer, and defines the set of transitions to subsequent states. A transition consists of the name of the target state and a predicate function.

When a test is run by Transition.js, the framework will call the test’s initializer, then:

If a predicate returns true, then the target state will be transitioned to. If no predicates return true, the framework will set a timeout (default is 250ms) before trying again. If the test takes more than a pre-set maximum (default 30s), then the test is considered failed. If it reaches a success state before the timeout, it is considered passed.

The Runner UI

Test Suite

The ‘Suite’ menu allows you to select a test to run, optionally by first filtering the list. You may also reload your test suite. Reloading does not effect the state of the Runner UI and is an effective tool when developing and debugging your tests.

The Test Log

Transition.js has a logging system that logs to the Runner UI instead of the browser’s debug console. The browser’s debug console is often either not visible on some devices, difficult to enable or not fully featured. The Transition.js logger is visible, supports searching (filtering) and setting different levels of output.

Test Controls

There are several control buttons in the runner UI:

Settings

Some settings can be changed via the test runner’s UI. Settings that are not exposed through the UI can be set from your test-suite.js:

// in test-suite.js
Transition.models.settings.set('pollTimeout', 250);
Transition.models.settings.set('logLevel',    Transition.Log.Levels.DEBUG);

// load your scripts here
// Transition.loadScript('/test/test-name.js');
int: frame-divider-upper-pct default=50
int: frame-divider-lower-pct default=50

These control the location of the divider between the main and test frames in the runner UI.

boolean: sortByLastModified default=true

If set to true, when tests are loaded via loadScript, the test suite will be ordered in reverse by time. The most recently modified test will be first, while The least recently modified will be last.

miliseconds: perStateTimeout default=10000

If a test stays in a single state for longer than this amount of time, the test will be marked as failed.

miliseconds: testTimeout default=30000

If a test takes longer than this amount of time, the test will be marked as failed.

miliseconds: suiteTimeout default=60000

If the test suite takes longer than this amount of time, the suite will fail.

miliseconds: pollTimeout default=500

This is the amount of time the framework will wait before checking the test’s current state to see if it should proceed to a subsequent state.

enum: logLevel default=Transition.Log.Levels.TRACE

This controls the verbosity of the log display. This value can also be adjusted via the Log drop-down menu. The following log levels are supported:

1. TRACE
2. DEBUG
3. INFO
4.. WARN
5. ERROR
6. FATAL

Defining a Test Suite

After creating the symbolic link to the Transition.js public directory, your next step is to define your test suite. You do this in the file test-suite.js in the same place you created the symbolic link:

public/
  +-- transition -> ~/projects/transition.js/public
  +-- test-suite.js
  +-- tests
      +-- add-a-list.js
      +-- add-todo-item.js
      +-- index-page.js
      +-- mark-item-as-done.js

Tests can be defined within the test-suite.js file directly, or in separate files. Best practice is to place utility code for testing your application in the test-suite.js and then each test in its own file:

(function () {
  var root        = this,
      TodoTestLib = {};

  this.TodoTestLib = TodoTestLib;

  TodoTestLib.deleteTestList = function () {
    ...
  };

  TodoTestLib.createTestList = function () {
    ...
  };

}.call(this));

Transition.loadScript('/tests/index-page.js');
Transition.loadScript('/tests/add-a-list.js');
Transition.loadScript('/tests/add-todo-item.js');
Transition.loadScript('/tests/mark-item-as-done.js');

Optinally Defining a test-suite.html for Static Content

If your tests themselves require helper libraries or other static content (like their own UI controls, html, etc.), you can create a test-suite.html file next to test-suite.js. The content of this file will be fetched and appended to the <body> before the test-suite.js file itself is loaded.

Defining Tests

Tests are added by calling Transition.addTest, passing an object of properties.

The test must have 2 properties:

Additional properties will become available as attributes on the Test model (available within your test with this.get('attr') and this.set('attr', 'value')).

Properties that are functions will be lifted to become methods on the Test model itself. Be careful not to re-define any existing Backbone Model methods.

States are constructed with Transition.newState. Each state must have:

Here is an example that loads the root page of the example TODO application and asserts that the form for creating lists is present:

(function () {

  this.addTest({
    name: 'Test Index Page',

    states: [
      this.newState('init', this.navigateTo_('about:blank'))
        .to('mainPage', this.constantly_(true)),
      this.newState('mainPage', this.navigateTo_('/'))
        .to('success').when('form[action="/lists"]')
    ]
  });

}.call(Transition));