Introducing React ReExt – Sencha Ext JS Components in React! LEARN MORE

Inside the Sencha Test Futures API

March 8, 2016 120 Views
Show

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!

Show
Start building with Ext JS today

Build 10x web apps faster with 140+ pre-build components and tools.

Latest Content
Discover the Top 07 Architecture Patterns used in Modern Enterprise Software Development
Discover the Top 07 Architecture Patterns used in Modern Enterprise Software Development

Developing software without an architecture pattern may have been an option back then. However, that’s…

JavaScript Design Patterns: A Hands-On Guide with Real-world Examples
JavaScript Design Patterns: A Hands-On Guide with Real-world Examples

As a web developer, you know how popular JavaScript is in the web app development…

Virtual JS Days 2024のハイライト
Virtual JS Days 2024のハイライト

2024年2月20日~22日、第3回目となる「Virtual JavaScript Days」が開催されました。JavaScript の幅広いトピックを採り上げた数多くのセッションを実施。その内容は、Senchaの最新製品、ReExt、Rapid Ext JSまで多岐にわたり、JavaScriptの最新のサンプルも含まれます。 このカンファレンスでは多くのトピックをカバーしています。Senchaでセールスエンジニアを務めるMarc Gusmano氏は、注目すべきセッションを主催しました。Marc は Sencha の最新製品「ReExt」について、詳細なプレゼンテーションを実施。その機能とメリットを、参加者に理解してもらうべく詳細に説明しました。 カンファレンスは、Senchaのジェネラルマネージャを務めるStephen Strake氏によるキーノートでスタートしました。キーノートでは、会社の将来のビジョンについての洞察を共有しています。世界中から JavaScript 開発者、エンジニア、愛好家が集まるとてもエキサイティングなイベントとなりました。これは、JavaScript…

See More