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!
![](https://staging.sencha.com/wp-content/uploads/2024/08/rapid-extjs-3.png)
We’re excited to announce the official release of Rapid Ext JS 1.0, a revolutionary low-code…
![](https://staging.sencha.com/wp-content/uploads/2024/08/arc-1.png)
The Sencha team is pleased to announce the availability of Sencha Architect version 4.3.6. Building…
![](https://staging.sencha.com/wp-content/uploads/2024/08/ReExt-Banners-Master-File-1.png)
Sencha, a leader in JavaScript developer tools for building cross-platform and enterprise web applications, is…