A while back, one of our engineers at Wealthfront wrote an in-depth article about deferreds. It’s important that if the functionality you build relies on deferred objects to trigger asynchronous events, that the behavior is properly tested. In this post I will go through 2 simple examples on how to test functionality that uses deferred objects. The first example tests functionality where a deferred object triggers handlers after an Ajax call is made with jQuery’s post() method. The second example tests functionality where a deferred object triggers handlers after a DOM element finishes a hide animation.
An example: Trigger the handlers of an Ajax POST call made with jQuery’s post() method
Recently, I updated how our front-end system handled users canceling their applications for a Wealthfront account. The example below shows a simplified example of what that code looked like:
Notice in this example that we are going to send an Ajax POST call when we click on the ‘#cancel-link’ link. We would like the ‘#cancel-status’ span to update whether the cancel application request successfully completed or failed. We have created separate handlers to update the DOM with the appropriate message through the .done() and .fail() methods. Using the .done() and .fail() methods help us allocate what work should be done in the event that the request to cancel the application was successfully or unsuccessfully processed.
If our request to cancel the application returned a successful response (status 200-299 or 304), the handler in the .done() method will execute and the html view will update with the status of whether the application was cancelled.
In contrast if system does not respond or responds with any other status, the deferred with trigger any functions defined in the .fail() method.
How can I use a deferred to test my .fail() and .done() handlers?
We would now like to test that the handlers properly update the DOM when the request is successfully or unsuccessfully returned. It’s undesirable to send the HTTP request every time a test is run because it slows the performance of the test, and it runs parts of the system that we are not interested in testing. Instead, we will mock the Ajax POST call to return our own local deferred object.
Note that at this point our local deferred object has added the two handlers responsible for updating the DOM with a successful and failed cancel transaction. You can see from the last assertion that the ‘cancel-status’ span text is still blank. To trigger the callback on the handler you update the deferred object with the .resolve() method to trigger the .done() callback as follows:
When we call the .resolve() method to trigger the callback, we have the option to pass in arguments that will be passed to the functions declared in the .done() method. In our example we pass in an object literal that has the ‘status’ property of either ‘success’ or ‘not-found’ status, to simulate the type of object that would be passed through the response of an actual Ajax call. Our assertions validate that our handlers properly received the object with the status to display the appropriate message. It’s not necessary to pass in an object when you are updating a deferred object with .resolve() or .reject(). The next example shows using the deferred object with the .reject() method with no arguments to trigger the .fail() callback:
Another example: Using deferred objects to trigger events from jQuery DOM animations
We want to test with an assertion that the ‘hide-status’ span appears after the link has been clicked. But if we take a straightforward approach, the test will fail. This is because the assertion is checked immediately after the hide is initiated, and still requires time before the hide completes and the span is updated.
While its reasonable to consider placing a timed delay before the assertion is checked, the approach could end up being inconsistent and hard to maintain. Here we can use a deferred object to have complete control over the completion of the hide. The next example shows two tests. The first test asserts that the ‘hide-status’ message returned is ‘in-progress’ as soon as the event is fired, but before the hide completes. The second test verifies that after the event has fired and the hide completes, the status message is changed to ‘finished’.
By mocking the call of the hide method to return a local deferred method, we can now be certain that the .done() callback has executed by the time the assertion is checked.
Checking for asynchronous conflicts with deferred objects
The hide animation example we were using actually has a flaw. When the user clicks on the link a single time, the page works as intended and hides the link with an animation. But what if the user clicks twice or multiple times on the link before the animation finishes? Multiple .hide() animations are queued and potentially reset the state of the link to visible as soon as the initial hide animation finishes. Since this is unintended behavior, we want to create a test to expect what we truly want, which is that only one .hide() is called no matter how many times the user clicks on the link.
The test above sets up the scenario where the hideLink method is called a second time before the animation completes. By using our own local deferred object in the test, we can prevent the .done() method from ever resetting the hideStatus value before the second call is made to hideLink. The test will fail in the current version of the code, but we can pass the test by adding a condition not to call .hide() again if the state of hideStatus is already ‘in-progress’.
The above version of the hideLink now only calls .hide() once if the user clicks the link multiple times and passes the test we just created. This example can be extended to many other cases that occur because of asynchronous conflicts. For example, you may need to check that a certain signup or buy transaction is only executed once if the button is clicked multiple times. The deferred object allows you to recreate these situations in tests that may not always be possible to isolate while running the code in real-time to verify your data and execution of code.
Useful Links on using deferreds: