Inside the Sencha Test Futures API

One of the biggest challenges of writing interactive tests is dealing with their asynchronous nature. Sencha Test offers a powerful new API designed to make these asynchronous tests almost as simple as their synchronous counterparts.

Asynchronous Testing in Jasmine

Testing asynchronous code is nothing new, and Jasmine provides solid support out-of-the-box by allowing your tests to mark themselves as asynchronous:

    describe('Some tests', function () {
        it('takes time', function (done) { // << "done" marks test async
            doThings().then(done);
        });
    });

The presence of the named argument on the function object (called “done” by convention) that is passed to the it() call indicates to Jasmine that the test is asynchronous. That is, the test will (eventually) inform Jasmine that it is finished by calling the provided “done” function instead of simply returning. If the test does not call the provided done function within a reasonable time (defaulting to 5 seconds), the test is declared a failure.

This simple approach works fine for small tests, but if there are several steps involved, it can be difficult to guess the total time things might take. Server requests and the like can be unpredictable even in a test environment, especially when that environment might be loaded up testing multiple browsers and scenarios simultaneously.

Of course, it is easy to forget to call done, but that is usually caught while the test is being written. If a test has complex logic involved, however, it is possible that some branches of code may call done while others do not.

    it('should eventually call done', function (done) {
        if (condition) {  // true 98% of the time
            done();
        } else {
            // oops - fails "randomly"
        }
    });

Clearly this kind of branched logic is best avoided when writing tests, if at all possible.

All of these issues are manageable until you enter the realm of interactive testing. This kind of test is commonplace in end-to-end testing of the application UI but also when unit testing application views and controllers. In both cases, most test steps require asynchronous operations and wait conditions, intermixed with correctness checks and expectations.

Element Futures

To make interactive tests expressive and maintainable, Sencha Test provides a family of classes under the ST.future.* namespace, collectively called “futures”.

Futures are objects, created in test code, that provide a concise syntax to describe an asynchronous test sequence. To see a simple example of futures, the following code uses an ST.future.Element that is returned by the ST.element() factory method:

    it('should change text on click', function () {
        ST.element('button#foo').
            click(10, 10).
            textLike(/^Save/).
            and(function (el) {
                expect(el.hasCls('somecls')).toBe(true);
            });
    });

The ST.element() method accepts a locator (a generalized string that locates an element using XPath, Component Query, etc). The returned ST.future.Element instance provides the methods we are using above: click(), textLike() and and(). Each method returns the same ST.future.Element.

It is important to keep in mind that while the calls made on futures look synchronous, they do not actually perform their operations immediately. Instead they are creating a scheduled series of actions that will occur “when they can”.

Locators

The first step in our test uses ST.element() to create the future element instance. While that is the first job of this method, its second and equally important job is to schedule a request to locate the desired DOM element. Under the covers, ST.element() stores the provided locator and starts waiting for that element to be added to the DOM and made visible.

The task of locating the element will not start until the test function returns control to the browser.

Actions

The second step in our test is to click() on the element using some (optional) element-relative coordinates. When the click() method is called, it adds a click event to the schedule. Many of the ST.future.Element methods are also action methods, and they all work in a similar manner: they schedule an action that will follow the previously scheduled actions, which will act on the element located by the future instance.

Action methods have verbs for names (such as “click”).

States

The third step in our test is the textLike() method. This schedules a wait for the textContent of the element to match the given regular expression. This group of methods is concerned with describing a state and injecting a delay in the schedule until that desired state is achieved. Some state methods require polling to detect the state transition while others can listen for events to detect the change. In any case, this optimization detail is something Sencha Test handles and is not a concern for the test author.

State methods have nouns or descriptions for names (such as “collapsed” or “textLike”).

Inspections

The final piece of the test is a call to the and() method. This method schedules the provided function to be called after the previous steps complete. In this case, after the textContent matches the regular expression. The function passed to and() will receive up to two arguments. In this case, we only declared one: the located ST.Element.

The optional second argument is a done function that works the same way as an asynchronous Jasmine test. If the function declares the second argument, the done function will be passed and must be called.

These functions are typically used to inspect the element, its current state and/or other aspects of the application.

Custom Waits

Sometimes a wait condition needs to be expressed in code. The and() function can be modified to accept a done function to allow the test to progress.

    it('should change text on click', function () {
        ST.element('button#foo').
            click(10, 10).
            textLike(/^Save/).
            and(function (el, done) {
                // wait for condition and call done()
            });
    });

In other cases, the test must simply poll for the proper state. Futures provide a wait() method to handle this:

    it('should change text on click', function () {
        ST.element('button#foo').
            click(10, 10).
            textLike(/^Save/).
            wait(function (el) {
                return el.hasCls('somecls'); // return true-like when done
            });
    });

In general, it’s best to use the and() approach because it avoids polling, but the right choice will depend more on the situation at hand.

Component Futures

Interacting asynchronously with elements is now easy using ST.element(), but the Sencha Test Futures API goes much further with ST.component() and related types of futures. These methods create instances of classes ultimately derived from ST.future.Component. These classes extend ST.future.Element and provide additional action and state methods appropriate to their type of component.

Consider a new example test:

    it('should change cell text on click', function () {
        ST.grid('grid#foo').
            row(42).
                cell('firstName').
                reveal().
                click(10, 10).
                textLike(/^Hello$/).
                and(function (cell) {
                    expect(cell.el.hasCls('somecls')).toBe(true);
                });
    });

More Locators

In this example, the first two calls after ST.grid() are locator methods: row() and cell(). There are various ways to describe the desired row and cell. These correspond to methods whose names start with “row” and “cell”. In this case, we are using the method that takes the id of the associated record (42) and the column id (“firstName”).

Once we’ve called a row() method, the chain of method calls will operate on that row future. We can “climb” back up to the grid by calling the row’s grid() method as shown here.

        ST.grid('grid#foo').
            row(42).
                reveal().  // scroll the row into view
                click(10, 10).
            grid().row(999).  // pick a different row
                reveal().
                click(10, 10);

This applies in a similar way to cells:

        ST.grid('grid#foo').
            row(42).
                cell('firstName').  // column id
                    reveal().  // scroll cell into view
                    click(10, 10).
                row().   // return to row 42
                cell('lastName').
                    reveal().
                    click(10, 10).
            grid().row(999).
                reveal().
                click(10, 10);

More Actions

The classes in the component futures hierarchy provide action methods for several of the most useful Ext JS framework methods. For example, ST.future.Panel provides collapse() and expand() action methods. These action methods schedule calls to the appropriate panel methods for the proper time.

More States

Component futures also provide additional state methods. For example, ST.future.Panel provides collapsed() and expanded() state methods that schedule a wait for the panel to be in the desired state.

Inherited States

Because ST.future.Component extends ST.future.Element, it inherits many of those action and state methods. This inheritance continues down the hierarchy of futures classes. For example, ST.future.ComboBox extends ST.future.TextField which extends ST.future.Field which extends ST.future.Component.

Interaction With Jasmine

Sencha Test integration with Jasmine is designed to allow futures to work with the Jasmine traditional style of asynchronous test. Even so, it is typically not necessary to use the Jasmine done function when using futures, as can be seen in the examples above.

When a series of future operations completes, the test will complete, and Jasmine will continue with the next test. Further, each step in a future sequence can control its own timeout value, so there is no need to determine one for the entire test. Because the timeout for each future action is also 5 seconds, it is often not necessary to set a timeout explicitly.

Another advantage of the futures API is that it provides a clean way to mix asynchronous actions and wait conditions with synchronous inspections using and(). This often results in not needing to use a done function at all and keeps test complexity to a minimum.

Keeping DRY

Futures enable tests to practice the DRY (Don’t Repeat Yourself) principle. Instead of creating the future instance at the point of need, consider the following alternative.

    describe('Many tests', function () {
        var Page = {
            fooGrid: function () {
                return ST.grid('grid#foo');
            },
            nameField: function () {
                return ST.textField('textfield[name="username"]');
            }
        };
 
        it('might take some time', function () {
            Page.fooGrid().row(42).
                reveal().
                click(10, 10);
        });
 
        it('might take some more time', function () {
            Page.fooGrid().row(999).
                reveal().
                click(10, 10);
        });
    });

As illustrated above, we are simply creating a set of methods collected in an object named “Page”. This approach allows the test to encapsulate the locators for the test subject (the application).

If the page object is useful to multiple tests, we can move it outside the describe() block and give it a more suitable name. Because Sencha Test will load all JavaScript files in the scenario, the page object will be available to all of the tests in the scenario. To share page objects across scenarios, they can be added to the Additional Libraries list for the test project.

Conclusion

We hope this gives you a taste of how the Sencha Test Futures API tames the complexity problem for asynchronous tests. Check out the API documentation for a list of all the action and state methods already provided and, of course, look for more coverage of Ext JS components and features in future releases. Happy testing!

Written by

Don is the Engineering Director for Ext JS and Sencha Touch. He was an Ext JS user for 2 years before joining Sencha and has more than 25 years of software engineering experience on a broad range of platforms. His experience includes designing web application front-ends and back-ends, native GUI applications, network protocols and device drivers. Don’s passion is to build world class products that people love to use.


Comments

  1. Developer says

    I’m still surprised that you developed this tool. First of all you should avoid using any kind of setTimoeut. Use viewModel.notify to bypass the internal scheduler. Unit test your tests with jasmine. Mock you backend and write functional tests. If you need more tests add end2end test. Execute tests on each build. Speed up and try to control your javaScript built by using gulp or grunt.

    • Don Griffin says

      Thanks for taking the time to reply, but perhaps there are some misunderstandings. Sencha Test supports multiple forms of testing: unit, integration and end-to-end/application. The tests described in this article are asynchronous which is a common case in the integration and app test domain. There is no direct use of setTimeout here, rather we are waiting on events (like focus or data loading).

      While we did not cover unit tests in this article, they are certainly the ideal foundation level for serious testing. We plan to write articles on using Jasmine (included in Sencha Test), but since that is a more well understood process we simply did not start with that topic.

      As great as unit testing is, however, it alone is not going to tell you that your app will load or that a user could interact with it. You can do that type of testing and write these async tests manually but that approach is more complex and hence error-prone. This type of testing really benefits from an API like Futures.

      Gulp/grunt/etc are just ways to run code so only indirectly related to testing, unit or otherwise. We’ll probably include discussions on these in upcoming articiles on Continuous Integration (CI) environments.

  2. Maciej Zabielski says

    Hi! nice article. As usual, some real life cases would be also nice to see :) I have tried to make use of that, and it seems to be working fine. The test code is much simpler!
    I’m testing if added user will actually apear on the grid list. If server call is successful, the grid is reloaded (we do not use auto sync function). I’m adding a record with a specific random value, so that test code can verify existence of this specific record (and not a previous failed test run for example)

    getFutureUserRecord: function(){
    return ST.grid(‘baseuserlistview grid’).rowWith(‘name’, testUser);
    }
    it(“user should be loaded into grid”, function() {
    HDTest.getFutureUserRecord().and(function(row){
    expect(row).toBeDefined();
    });
    });

    After that the test will identify index of the added user,
    getUserIndex: function(){
    var grid = Ext.ComponentQuery.query(‘baseuserlistview grid’)[0];
    var st = grid.getStore();
    return st.find(‘name’, testUser);
    }

    and try to remove the record using GUI functions
    it(“should delete user”, function() {
    var index = HDTest.getUserIndex();
    ST.play([
    { type: “tap”, target: “grid => [data-recordindex=\”” + index + “\”]”, x: 88, y: 22, delay: 50},
    { type: “tap”, target: “button[iconCls=\”icon-user–minus\”]”, x: 47, y: 16}
    ]);
    });

    Now, we have to wait for the ajax call that is performed after handler is triggered for delete button.
    When the ajax returns, the grid will be reloaded again. The record should not be present.
    Here futures have no use. What approach would you recommend to detect if something is “not present”, but the detection must be done after all ajax calls returned and were processed. Using Jasmine Spy On and expect(foo.getBar).toHaveBeenCalled(); or somehow utilize Jasmine Async support? It’s not clear for me yet on how to detect from a test that some async sequence was completed.

    Thanks for any tips!
    Regards,
    Maciej

    • Maciej Zabielski says

      I think I have found quite nice solution that works in my case. If I know the last store that will be reloaded as a result of a delete button, I can attach to it’s load event and utilize passed “done” function from Jasmine.

      it(“should delete user”, function(done) {
      index = HDTest.getUserIndex();

      ST.play([
      { type: “tap”, target: “grid => [data-recordindex=\”” + index + “\”]”, x: 88, y: 22, delay: 50},
      { type: “tap”, target: “button[iconCls=\”icon-user–minus\”]”, x: 47, y: 16 }
      ]);

      var grid = Ext.ComponentQuery.query(‘baseuserlistview grid’)[0];
      var st = grid.getStore();
      st.on(‘load’, done, this);
      });

Leave a Reply

Your email address will not be published. Required fields are marked *