Chapter 9. Asynchronous programming with callbacks and futures

This chapter covers

  • The nonblocking async programming model
  • Callbacks for asynchronous APIs
  • Improving asynchronous readability with futures and completers
  • Unit-testing asynchronous code

In web programming, you can’t rely on events outside your application’s control happening in a specific order. In the browser, retrieving data from a server might take longer than you expect, and instead of waiting for the data, a user might click another button. A Dart server application will likely need to handle a new request for data before a previous request has finished reading data from the file system. This type of programming is known as an asynchronous model (async), and its counterpart is the synchronous model. In a synchronous model, everything happens in order, waiting for the previous step to fully complete. This is fine for some environments, but in a web application environment, you can’t block all execution while you wait for the previous task to complete.

This is a powerful but nontrivial programming model that’s also used in JavaScript. We’ll spend some time in this chapter getting to grips with async programming and exploring the nonblocking aspect of Dart’s event loop. In JavaScript, you use callback functions with the async programming model, and you can do the same in Dart. We looked at callback functions back in chapter 4, because they’re a common pattern in async programming, and we’ll revisit them in this chapter. Callbacks don’t come without problems for readability, maintainability, and sequencing, as you’ll discover, but Dart introduces a new pair of types to address these problems: Future and Completer. A Future represents a future value—a value you know you’ll have at some point in the future—and is perfect for async programming, so we’ll also spend some time looking at this pair of types.

Finally, you’ll use your new knowledge of async programming to write some unit tests that are specifically able to cope with async code. Unit tests normally run sequentially, with the unit-test app exiting once the last test has run, but this pattern doesn’t work when your code is still expecting a response from some external influence. Fortunately, Dart’s unit-test library allows you to wait for async calls to complete before exiting, as you’ll see in action at the end of the chapter.

First, though, we should look at what happens in a synchronous, blocking web app. Many countries have a regular lottery in which numbered balls pulled from a machine represent the winning numbers for that week. Players check their tickets against the winning numbers. In order to build suspense and excitement, the numbered balls appear from the machine at random intervals. You’ll build this first as a synchronous app, which will cause the browser to freeze until all the winning numbers are generated, and then you’ll fix it to use correct async APIs, allowing the browser to remain responsive. Figure 9.1 shows the problem you’ll experience with the synchronous version of the app.

Figure 9.1. The synchronous version of your Dart Lottery app will block until your code finishes executing.

9.1. Why web apps should be asynchronous

You’re going to build a lottery web app to simulate a lottery game. In the app, when you click the Start button, three winning numbers are generated, each after a random delay of up to two seconds. This delay will cause the browser to lock up until it responds. The code to implement this delay gets the current time and waits in a while loop until it has waited the correct amount of time. Each winning number is displayed on the UI. Listing 9.1 shows the complete app’s code.

 

Warning

The code in listing 9.1 is bad in a web programming environment. It’s included here only to demonstrate the impact of a blocking web app. In the next section, you’ll see the correct technique to wait a given period of time using async APIs.

 

Listing 9.1. lottery_app.dart written in a synchronous, blocking style

This code is straightforward: you read down the startLottery() function to see that it retrieves each winning number in turn and updates a specific <div> in the UI with that winning number. Unfortunately, this isn’t what happens in practice: the browser has an event loop that processes tasks that are waiting on a queue, and because your code blocks the execution flow, your UI updates aren’t acted on by the browser event loop until the code has finished executing. The longer you spend in each of your getWinningNumber() functions, the longer the browser has to wait until it regains control and can start processing the event loop again. Figure 9.2 demonstrates the flow that the browser takes when processing your code and its event loop.

Figure 9.2. The browser event loop processes events only when your Dart code isn’t executing.

In practice, many of the tasks a browser needs to perform, such as interacting with a server, are carried out on different threads internally in the browser, so the event loop only needs to start the task and be notified (again, via the event loop) that a task has finished. But it can’t do this while your code is blocking, so you need to change the code to ensure that it executes and exits as quickly as possible—a task made possible by the asynchronous APIs.

As you convert this app to be asynchronous, you’ll build up a set of APIs for running the lottery app in an async manner. To properly separate the UI from the API, make sure to properly organize the code into separate files. The lottery_app.dart file will contain your app, which interacts with the UI, and the lottery library will contain your API. Figure 9.3 shows this relationship.

Figure 9.3. The relationship between the lottery app and the lottery library

This split will help you later when you provide async unit tests for the lottery.dart API functions.

9.1.1. Modifying your app to be asynchronous

Now that you have an app structure, you can start to modify it to become asynchronous. An ideal way for the Dart lottery app to work is to provide some suspense and drama by creating winning numbers in any order and displaying each number on the UI as it appears. Thus the second number could appear first, followed by the third and first, with each dependent on the random time taken for each number to appear.

Figure 9.4 shows the UI you’ll build and the browser console with some of the logging. As you can see, although the winning numbered balls are requested in order, they’re generated out of order.

Figure 9.4. The Dart Lottery app pulls numbers after a random amount of time.

You might have noticed that the Dart Lottery pulls only three numbered balls, so it’s significantly easier to win than most lotteries.

Real-World Delays

The time delay from when the app starts until the numbered balls appear represents a nice async flow that you need to cope with in a client-side app. In the real world, this async flow might come from requesting data from three different server requests or waiting for three different inputs from a user. In the Dart Lottery example, the data you’re waiting for is the number pulled from the machine for each ball, and a random timer provides the I/O delay.

In the synchronous version of Dart Lottery, you wait a random amount of time before each ball is pulled from the machine, and no other ball can be drawn until the previous one has completed. But the async lottery allows multiple balls to be pulled from the machine in any order. It’s possible for the third ball to appear from the machine first. The async pseudocode flow is as follows:

Beginning Async Programming with Window.Settimeout()

The core of the lottery app is the lottery library. This library provides the code to return a random number after a certain amount of time. The lottery library uses an async API built into the web browser called window.setTimeout(callback, duration), which executes a callback function after a certain number of milliseconds. A trivial usage of it is shown in the following snippet, with the callback event handler in bold italic:

The Dart app calls window.setTimeout() and then continues executing, finally returning control to the event loop. The event loop calls the setTimeout() function’s callback event-handler function only when the specified milliseconds have elapsed. This is the principle of the event loop and asynchronous programming: the code starts a task and returns control to the event loop, which notifies the code when that task has completed.

Figure 9.5 shows how window.setTimeout() interacts with the event loop in an asynchronous manner.

Figure 9.5. Async calls mean that control returns to the event loop as quickly as possible.

This asynchronous handling of events in the order in which they occur also happens in other APIs, such as requesting data from a server using HttpRequest. Your app requests data from the server and returns control to the event loop. The event loop calls the anonymous event-handler function once the server has responded with data. An example is shown in the following code snippet; the anonymous callback function is shown in bold italic:

HttpRequest.get("http://example.com", (data) {
   // handle data being returned
});

In the next section, you’ll use the async setTimeout() function as you start to use callback functions to interact with the async APIs provided with the browser. We first looked at callback functions in chapter 4, and now that you know the look and feel of the lottery app, it’s time to revisit them in the context of async programming.

 

Remember

  • Synchronous code executes in sequence, waiting for each blocking operation to complete in turn.
  • Asynchronous (async) code doesn’t block. Instead, the event loop is responsible for notifying the app when an I/O task completes.

 

9.2. Using callbacks with async programming

Your app is split into two files: the lottery library, which represents the async API and provides the useful functions to generate numbers and eventually sort them into order, and the lottery_app.dart file, which contains your app’s main() function and imports the lottery library. The main() function is the first function that executes when a Dart app starts, and splitting the key functions into a separate lottery.dart file will help later when you refactor and test your async code.

The first async version of Dart Lottery uses callback functions to retrieve numbered balls. This programming model is common in JavaScript development, both on the client and the server side, and is possible because functions can be passed as parameters to other functions.

 

Note

Callbacks are a common pattern, and you should get used to reading code that uses callbacks, but they do have their drawbacks, as you’ll discover. In the next section, you’ll see how you can improve existing APIs that use callbacks by using the Future and Completer pair of values.

 

The basic callback code, which is still running in a synchronous fashion but without the delay, is shown in figure 9.6. This is a simplified version that outputs a single winning number to the browser console.

Figure 9.6. The lottery.dart file defines a getWinningNumber() function that takes a callback parameter. The app passes a callback to getWinningNumber(), which is called when a number is retrieved.

Nothing in the code shown in figure 9.6 represents async programming; instead, the code is entirely synchronous. The main() function is called, followed by the call to getWinningNumber(), which accepts a callback function as a parameter. getWinningNumber() creates a random number and passes it back into the callback function, which outputs the winning number to the console. Only after these steps have occurred does control return to the event loop. This is fine, because there is also no blocking code yet. The lottery app can call getWinningNumber() three times, and three winning numbers will be printed to the console.

To improve the app’s UI slightly, you’ll add a utility function updateResult(int ball, int winningNumber) that will populate a <div> element in the browser representing a ball with the winning number. lottery_app.dart now contains the code shown in the next listing, which uses both shorthand and longhand function syntax to call updateResult() with each winning number.

Listing 9.2. lottery_app.dart: using callbacks

The associated API library lottery contains the single getWinningNumber() function, which accepts a callback function and generates a random number, as you saw in figure 9.6. In a moment, you’ll modify this function to use the async API call window.setTimeout() and add some suspense and drama to the lottery app.

9.2.1. Adding async callbacks to Dart Lottery

Now the async Dart Lottery can begin. You can generate random numbers and display them on the UI. Because the code is synchronous, executing one statement after another, it will create result1 first, result2 second, and result3 third.

But there’s no suspense (and no async code), because you’re pulling the numbers out of the machine as fast as the code will execute. Fortunately, it’s easy to introduce suspense without changing lottery_app.dart: modify the getWinningNumber() function in the lottery library to use the window.setTimeout() function to ensure that the callback is called only after some random amount of time, which will cause the results to be generated in any order. Perhaps result3 will appear first, closely followed by result1 and, after a final delay, result2. The next listing modifies getWinningNumber() to call the callback function after a random amount of time of up to two seconds.

Listing 9.3. lottery.dart: adding a timer to getWinningNumber()

 

Tip

For animation, the HTML5 browser function requestAnimationFrame() is a better choice for updating the screen periodically. This is because its refresh frequency is determined by the capabilities of the browser and hardware, and it runs only when the browser tab is visible.

 

The three calls from main() to getWinningNumber() happen in turn as fast as the code will execute. But because they took place before you added the setTimeout() call, the actual results will be generated at some unknown point in the future. Once all three calls to getWinningNumber() have been made, flow returns to the event loop, which responds again only when the setTimeout handlers need to be called after each of their timeouts expires, as shown in figure 9.7.

Figure 9.7. The code runs as fast as possible until the main() function finishes executing. At that point, the event loop waits for the timers to time out, calling back into the Dart code.

This is exactly how you can expect code in the real world to work. When you request data from a server, you don’t know how long the server will take to respond. The user may have clicked another button in the meantime, triggering a different request to a server. The app could receive the server callback events in any order.

9.2.2. Ensuring that all async callbacks are complete before continuing

Dart Lottery needs to do more than just show the numbers on the screen as they’re pulled from the machine. Once all three balls have been pulled, you need to display them neatly. To do this, you’ll introduce a new utility function called getResultsString (List<int> winningNumbers, String message). It will return a string containing the message and the list of comma-separated winning numbers.

There’s now a requirement for some sequencing in your app. You can get the three numbers and display them onscreen in any order, but only after all three numbers have appeared do you want to execute the next part of the app that displays the results string.

This approach creates some complexity in async code, because each of the callbacks has no knowledge of the other callbacks. Thus you need to introduce some check or another mechanism. There are at least a couple of ways of doing this. The first is to store a flag in the main() method outside each callback and have each callback check whether all the values have been retrieved. The second is to nest the callbacks, in which case you’ll look at each in turn.

Listing 9.4 shows a modified lottery_app.dart in which the callback functions call an addAndDisplay() function that’s declared in main() to add each result to a result list defined in main(). Only when that list contains three items does addAndDisplay() call getResultsString() and display the list’s contents in a browser <div>. The addAndDisplay() function also becomes a closure because it’s passed into each callback, retaining access to the results list variable, even when it’s called in the scope of getWinningNumber(). Please refer to chapter 4 for a recap on closures.

Listing 9.4. lottery_app.dart: introducing sequencing into an async callback

Although this code works fine, it dramatically increases the app’s complexity. As you read the code from top to bottom, it’s no longer possible to see the exact order in which things occur. You can only tell by the logic in the addAndDisplay() function that the winningNumbers <div> will be populated after all three callbacks have occurred.

Fortunately, a second approach—nesting callbacks—can provide a limited amount of readability benefit and ordering, at the expense of allowing all three functions to execute simultaneously. This approach is often beneficial, though; many times you’ll want to simulate a synchronous flow even when you’re dealing with an async API. For example, you might need to be sure that you’ve retrieved data from server 1 before you retrieve data from server 2 or that you’ve connected to the database before you query it.

9.2.3. Nesting callbacks to enforce async execution order

Nesting callbacks is a technique that allows you to simulate synchronous code when you have only an asynchronous API, such as with getWinningNumbers(). This technique is used often in JavaScript, especially with server-side Node.js or a Dart VM to execute async code in the correct, logical order, such as open a file, read the file, close the file. All of these are async tasks, but they must be performed in the correct order. There’s a big downside to this technique, though. Once you get more than three or four nested callbacks, readability again becomes a problem, as you’ll see.

When nesting callbacks, you need to ensure that the first callback calls the second getWinningNumber() function and the second callback calls the third getWinningNumber() function, and so on. The last callback can then execute the final step in the chain, such as displaying the list of results on the UI.

Listing 9.5 modifies lottery_app.dart using nested callbacks to ensure that the winning numbers are drawn in order and that the list of winning numbers is updated only after the third number is drawn.

Listing 9.5. lottery_app.dart: using nested callbacks

As you can see, with three levels of nesting, things are starting to get complicated. The next requirement for the app is to sort the balls into order, also using an async API, which means another nested callback in the third callback.

Unfortunately, this requirement is all too common in the real world, where you only have async APIs to work with but you need to either enforce a specific order or wait for a number of async calls to complete. A real-world example on the client side is a button-click handler to retrieve data from a server call, manipulate that data, and send the data back to the server, alerting the user when complete; a dive into many JavaScript applications will show code that contains nested async calls many levels deep, which is popularly known as callback hell. The following snippet shows how it might look if you had six balls instead of three:

getWinningNumber( (int result1) {
  updateResult(1, result1);
  getWinningNumber( (int result2) {
    updateResult(2, result2);
    getWinningNumber( (int result3) {
      updateResult(3, result3);
      getWinningNumber( (int result4) {
        updateResult(4, result4);
        getWinningNumber( (int result5) {
          updateResult(5, result5);
          getWinningNumber( (int result6) {
            updateResult(6, result6);
            //snip getResultsString()
          });
        });
      });
    });
  });
});

Many frameworks in the JavaScript world have been created to try to deal with this callback-nesting problem, but Dart brings its own solution to the table in the form of the Future and Completer types. These provide neat mechanisms to let async code execute in a specific order without nesting and to allow code to continue executing only after all the async operations are complete.

 

Remember

  • Callbacks provide the standard async pattern of operation.
  • A callback function that’s passed to an async API will be called when the async operation is completed.
  • To ensure that all async operations are completed before the next block of code executes, you can maintain a count or other flags to indicate that all the async operations have completed.
  • To enforce a specific sequence of code execution with async APIs, you can nest API callbacks.

 

9.3. Introducing the Future and Completer pair

You saw in the previous section how to run async code using callback functions. When you request a winning number from the Dart Lottery machine’s getWinningNumber() function, you’re making an async request that returns a winning number after a random amount of time. Once all three of the winning numbers have been returned, you perform the next step in the app’s flow: formatting the numbers for use on the UI.

The code to check whether all three numbers have been returned became more complex, and you lost the ability to easily navigate the code. Fortunately, Dart provides a neat pair of types, Future and Completer, that will help you write more maintainable and readable async code.

These two types work closely together to return a future value, which is a value that will exist at some point in the future. A Completer object is used to return an object of type Future, which represents a future value. This future value is populated when the completer.complete(value) function is called, passing in the real value for the future. You can wrap your existing async API call to getWinningNumber(callback) to instead return a future value. Figure 9.8 shows how to achieve this; we’ll then look at how to use a future value.

Figure 9.8. It’s possible to wrap an async callback API to use the Future and Completer types.

When you call getFutureWinningNumber(), you get back a result immediately in the form of a Future<int> returned from a completer. The code continues to execute, using that future value in place of a real value. In getFutureWinningNumber(), you’ve made a call to the async getWinningNumber() function, which itself is requesting data from an async API. At some point, when the completer’s complete() function is passed the real value, the future value will finally contain a value. How do you access that future value? Once again, via a callback, which is passed into the future’s then() function.

Let’s see this in action by replicating the first simple callback example to display three numbers. The next listing shows the new lottery_app.dart file, which uses getFutureWinningNumber(), and the original version with callbacks for comparison.

Listing 9.6. lottery_app.dart using Future values and then() callbacks

A more concise way of writing this code is to chain the then() function onto the original function call:

getFutureWinningNumber().then((int result1) => updateResult(1, result1));

In terms of functionality, it appears you’ve gained little. You still provide a callback into the then() function, and each callback is executed when the future value has a value (with the future value being passed on the callback parameter). What “extra value” does a future value give you?

9.3.1. Passing around future values

The first thing you can do with a future value is pass it around the application. With the callback version, you need to decide what you want to do with the winning number when you call getWinningNumber()—in this example, by passing the winning number into the updateResult() function.

With the future version, you can put off that decision and allow another part of the code, such as an updateUserInterface() function, to decide what happens to the future value by passing in the futures as parameters. This approach has the twin benefits of reducing callback nesting and allowing you to pass all three future values into another function, even though the async result that they represent hasn’t yet been returned. You can refactor the lottery_app.dart file again to pass the future values into updateUserInterface(), as shown next.

Listing 9.7. lottery_app.dart: passing future values into a function

This is a powerful feature of futures. Real-world scenarios include passing a number of future data values retrieved from the server into a UI component and handling a user’s “future” button click by passing it to a function in the app. Because the future values are variables that know they’ll have a value in the future (via the then() function), it’s easy to store them in other classes such as lists. This brings us to a second powerful feature of futures: performing async calls in sequence.

9.3.2. Ordering async calls by chaining futures

Back in the callback discussion, async callbacks were nested in each other to ensure that they were executed in order. When you get a few callbacks deep, you can end up in nested callback hell, in which you’re nesting so deep that readability and indenting start to be a problem. This nesting effect can be achieved with futures by embedding each future in the previous one’s then() function, as shown in the following snippet:

getFutureWinningNumber().then( (result1) {
  updateResult(1, result1);
  getFutureWinningNumber().then( (result2) {
    updateResult(2, result2);
    getFutureWinningNumber().then( (result3) {
      updateResult(3, result3);
    });
  });
});

This way is no better than using nested callbacks, so although it’s possible to nest futures, doing so is clearly a suboptimal solution. Fortunately, Dart’s futures provide a mechanism to chain futures together so the next one is executed only when the previous one finishes, without nesting. This mechanism is achieved via the future.chain(callback) function, which is used in place of future.then(). The chain() function allows you to return another future, providing a mechanism to chain futures together, as shown in the following listing.

Listing 9.8. lottery_app.dart: chaining futures together to enforce ordering

As you can see, you can continue to chain many futures together without nesting or affecting readability. Each call to getFutureWinningNumber() is made only when the previous call has completed.

9.3.3. Waiting for all futures to complete

In the discussion on callbacks, results were to be displayed as a formatted string using the getResultsString() function, but only after all three winning numbers had been retrieved. You achieved this by retrieving each value asynchronously and having each callback call an addAndDisplay() function that added the value to a list and performed the display action only when there were exactly three items in the list (one for each async value). This solution, although it works, introduces complexity in what should be a straightforward piece of code. You want to make three calls to getWinningNumber(), and only when all three calls have completed will you execute the next step.

This is one area in which future values shine. Dart provides a Futures.wait(futures) function that takes a list of future values and returns a single future. The single future contains a list of all the values returned from the futures passed in. Although this sounds complex, it’s simple to use in practice, as shown in figure 9.9. The function waits for three real values to be returned from three future values and then passes the three real values—the three winning numbers—to be formatted on the display.

Figure 9.9. Futures.wait() allows you to wait for many futures to complete before continuing execution.

By passing all the future values into the wait() function, you can be sure all the futures have completed and returned real values before you continue to the next step. Because wait() also returns a future value, you can use its chain() function to chain it to other futures. This is helpful if, for example, you want to wait for the three numbers to complete and then request a fourth “bonus ball” number, as shown in the next listing. In this listing, you also add the future value results from getFutureWinningNumber() directly into a list.

Listing 9.9. lottery_app.dart: waiting for futures and chaining

By using the then() and chain() methods of the Future class and the wait() static method of the Futures class, it’s possible to write readable async code that avoids multiple levels of nesting and preserves order. There’s still one more method of the Future class to look at: transform().

9.3.4. Transforming nonfuture values into futures

You saw earlier how to take future values and pass them around your application. This approach is fine when it’s the future values you need, but sometimes the future values are just a means to an end. In the Dart Lottery app, you’re interested in the results string, which is generated by the getResultsString() function. It’s the string value returned from getResultsString() that you want to pass around your app. Because a number of async calls need to complete before you can get the results string, you can’t just call getResultsString() and pass its value to another function. Instead, you can call getResultsString() in the final then() handler of the final future, but sometimes you also need to use another future value. The result can be nested futures, which is what you’re trying to avoid with futures in the first place. The following snippet shows the problem:

At this point, the transform(callback) function comes in. It’s similar to the chain() function in that it returns a future. The difference is that chain() expects its callback function to return a future, and transform() wraps any value in a future, allowing you to return a future value as part of a sequence of async method calls—even when some of those method calls aren’t async.

Listing 9.10 shows transform() in action: it waits for all three winning numbers to be drawn, passes them to getResultsString(), and automatically wraps the return value from getResultsString() into another future that you can use to chain to the next call to formatResultString(), which is used by the updateWinningNumbersDiv() function. This avoids nesting the call to formatResultString() in the wait().then() callback function.

Listing 9.10. lottery_app.dart: transforming a nonfuture value into a future value

Future values, supported by their completers, provide a powerful mechanism for writing code that uses async APIs in a readable and maintainable manner. By chaining async calls with chain() and nonasync calls with transform(), you can mix asynchronous and synchronous code while maintaining a coherent flow.

Many Dart async APIs return future values rather than using callbacks, but some still do use callbacks. In this section, you’ve seen how to wrap callback APIs such as getWinningNumber(callback) to return future values by hiding the callback functionality in a wrapper function, instead returning a future value that’s completed when the callback itself is called.

 

Remember

  • You can wrap async callback API functions to return future values.
  • The future value’s then(callback) callback function is called when the future has finally received a value.
  • transform(callback) wraps its callback’s returned value in a new future.
  • chain(callback) expects its callback’s returned value to be a future.
  • The chain() and transform() callback functions are called when the future receives its value but also let you return another future. You can use these to build a sequence of multiple async and synchronous calls.

 

In the final section, we’ll look again at unit testing, which we last discussed in chapter 3, and you’ll see how to unit-test the async library functions.

9.4. Unit-testing async APIs

Dart’s unit-testing framework allows you to test your functions in a separate script with its own main() function. This script imports the library APIs and tests the various pieces of functionality you’ve built. When you write unit tests, you expect each test to run some code, complete, and report whether that test passed or failed, depending on your expectations. The following listing shows an example unit test to test the getResultsString() function from the lottery library. This code is similar to the testing code we looked at earlier in the book.

Listing 9.11. lottery_test.dart: unit-testing getResultsString()

This is a standard, synchronous unit test. The flow follows the expected execution sequence:

1.  Start the main() function.

2.  Start the test() function.

3.  Call getResultsString() with sample parameters.

4.  Check the expectations on the return value.

5.  The test() function exits.

6.  The main() function exits.

You can test async APIs such as getWinningNumber(callback), which uses callbacks, or getFutureWinningNumber(), which uses futures and requires a slightly different approach.

Because of their asynchronous nature, the test() and main() functions will have exited before the callback or future value is returned, as in the following sequence:

1.  Start the main() function.

2.  Start the test() function.

3.  Call getFutureWinningNumber() with sample parameters.

4.  The test() function exits.

5.  The main() function exits.

6.  The future value is returned.

This sequence presents a problem because it’s the future value that you need to check against your expectations, and test() and main() have already exited. Fortunately, the Dart unit-test framework contains a number of expectAsync functions in the form of expectAsync0(), expectAsync1(), and expectAsync2(), which wrap the callback that you’d pass into the async API functions or the future value’s then() function. The numerical suffix on the expectAsync functions represents the number of arguments the callback function expects. Because future callbacks only ever have a single argument (representing the real value used to populate the future) passed to them, this is perfect for our example. Likewise, the callback version of your API also returns a single value, so you can use expectAsync1() in both cases. Let’s test the callback version of your API function with getWinningNumber() first and then test the getFutureWinningNumber() function.

9.4.1. Testing async callback functions

The expectAsync function wraps the callback function passed to getWinningNumber(). The following snippet is a reminder of the call to getWinningNumber() and its callback function:

Calling this code is fine in the context of a web browser, because once you call getWinningNumber(), control returns back to the event loop. But in the context of a unit test, you need to ensure that the unit test waits for the result value to be returned before it exits. This is where the expectAsync() function comes in: it wraps the callback, which forces the unit-test framework to wait until the async call has completed before exiting. This gives you the ability to check your expectations of the result value and is shown in the following snippet, with the expectAsync1() function highlighted in bold italic:

getWinningNumber( expectAsync1( (int result) {
  // ...snip ... test the result value
}));

In the simple test shown in the following listing, a real unit test verifies that the number returned is in the range 1–60 inclusive.

Listing 9.12. lottery_test.dart: testing async callback functions with expectAsync()

Now that you’ve seen how to test async callback functions, let’s apply the same knowledge to async functions that return future values.

9.4.2. Testing future values

The final step in async testing is to test the result of getFutureWinningNumber(). A future value’s then() function takes a callback function to receive the final value. It’s this callback function that you wrap in the expectAsync1() function, which lets you check your expectations on the future value returned. The following snippet shows the test code for testing the future value, with expectAsync1() highlighted in bold italic:

test("Future winning number", () {
  getFutureWinningNumber().then( expectAsync1( (int result) {
    expect(result, greaterThanOrEqualTo(1));
    expect(result, lessThanOrEqualTo(60));
  }) );
);

You can even test multiple futures by using the Futures.wait() function and wrapping its then() function in an expectAsync1() function, as shown in the next listing, which expects three values to be returned in the list.

Listing 9.13. lottery_test.dart: testing multiple futures with wait() and expectAsync ()

When checking futures, the future value’s then() function callback needs to be wrapped by expectAsync1(). This also means you can use .chain() and .transform() to link futures together in a specific order. Finally, you wrap the last future’s then() function callback in expectAsync1() in a manner similar to that of the previous listing.

 

Remember

  • Testing async APIs requires special treatment; otherwise, the unit test will exit before the returned value has been checked.
  • The callback function passed either to the async API function or into a future’s then() function needs to be wrapped by an expectAsync() function, which forces the test framework to wait for the async call to complete.

 

9.5. Summary

There’s no doubt about it: asynchronous programming is harder than synchronous programming. But nonblocking async APIs provide your app with the means to stay responsive, even when multiple requests are running that would otherwise block execution. On the server side, this could be file system access and network socket access; on the client side, it could be requesting data from a server or waiting for a user to enter some input. Dart’s async APIs allow your code to execute and return control to the event loop, which will call back into the code again only when something happens, such as a file read completing or a server returning data.

Callback functions will be familiar to JavaScript developers, but many nested callbacks, which are often needed to enforce execution sequence, can create a callback hell. Dart allows callback functions but also provides the Future and Completer pair of types that work together to provide a future value that can be passed around your app. Only when the async call completes does the future value get a real value.

Multiple async requests can be chained together using the chain() function, which lets you avoid many nested callbacks. The future’s transform() function also lets you use synchronous APIs interspersed with async APIs. The wait() function lets you wait until all futures have received real values before you continue processing.

Finally, unit-testing async code uses the expectAsync functions, which let the unittesting framework know that it should wait for the callback or future value to be returned before the test is complete. These functions let you test your own async APIs in the same way you test standard, synchronous code.

Now you know about nearly all the concepts in the Dart language, and you’re ready to start building apps. In the next section, you’ll see more interaction with the browser, and we’ll look at how to build a single-page web application with multiple views, offline data storage, and interaction with servers.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.189.180.43