Test the Thread Task Directly

Object-oriented threading libraries generally separate the modeling of a thread from the modeling of the task that the thread runs. Usually, there are also convenience facilities to just use the thread model directly rather than explicitly creating separate instances for each. From its inception, Java has had a Thread class and has represented the task with the Runnable interface. A Thread is a Runnable, and its default implementation runs itself as its task.

Shortcomings in this design3 motivated the addition of the java.util.concurrent packages to Java 5. In addition to a broad range of new synchronization facilities, it added Executor as the model of execution and Callable as the model for the task. The new packages use and support the older thread-related classes out of both convenience and necessity.4

3. Among others, the mechanism to communicate exceptions out of your Runnable was awkward and sometimes buggy, and there was no formalization of a return value from the task.

4. Executor is not really a replacement for Thread; rather, it is an abstraction of a wrapper around Thread. While Callable is an improvement and replacement for Runnable, there are a number of convenience constructors and methods that take a Runnable to ease the transition to the newer library. There are also a large number of Java libraries that expect a Runnable and were designed before Callable existed.

Both Runnable and Callable are defined as interfaces, which helps us untangle some of the testing issues. As interfaces, they represent the methods necessary to execute on a thread, not the actual work done on the thread. This tells us that a task incorporates two distinct purposes, which we may test separately as appropriate. The first is the ability to execute on a thread. The second is the functionality that will be executed on that thread. Examining the task functionality, we will usually see that there is nothing in the task that requires that it be executed on a separate thread.

Wait a minute! Why wouldn’t a thread task need a separate thread? The nature of many multithreaded algorithms is to segregate most or all of the data such that there is no need to synchronize the access. The synchronization occurs when the data results are joined together. You have to wait for the results anyway. Joining finished results while waiting for others to come in speeds up the computation. This is the essence of MapReduce5 frameworks and other parallel data partitioning schemes. This means that many tasks do not require any synchronization and can be tested for their computational purpose directly!

5. See the original Google Research paper on MapReduce at http://research.google.com/archive/mapreduce.html.

So how does this affect our design for testability? First, it suggests that we should ensure that our task is encapsulated in its own testable unit. While it is convenient to implement the task as simply an extension of a Thread, we establish a separation of concerns that better supports testability by encapsulating the task in its own class. Second, it is common to use inner classes and anonymous inner classes as the tasks. This usage often hides the details of the task from outsiders who may not need to know them, but it also hides the task from tests. Our tasks are more accessible to tests and therefore more testable if they are modeled as named classes rather than hidden or anonymous ones.

This same principle applies in procedural languages, by the way. For example, the thread library Apple provides for threading on OS X and iOS is distinctly procedural, requiring either function pointers or Objective-C blocks6 as the task definition. Although it can be convenient to define the tasks inline in this manner, it can make the overall code much harder to unit test.

6. The Objective-C implementation of closures. The closest analogy of this in Java prior to Java 8 lambdas is anonymous inner classes.

Let’s look at a brief example of refactoring to a separate Runnable in Listing 13-5.

Listing 13-5: Implementing a parallel computation directly as a Thread

public void parallelCompute() {
  Thread thread = new Thread() {
    public void run() {
      Result result = computeResult();
      storeResult(result);
    }
    private Result computeResult() {
      ...
    }
    private void storeResult(Result result) {
      ...
    }
  };
  thread.start();
  // Do some things while the thread runs
  thread.join();
}

Separating the Runnable from the Thread to a named inner class gives us the code in Listing 13-6.

Listing 13-6: Refactoring Listing 13-5 to extract the computation into a Runnable

public void parallelCompute() {
  Thread thread = new Thread(new ComputeRunnable());
  thread.start();
  // Do some things while the thread runs
  thread.join();
}

private class ComputeRunnable implements Runnable {
  public void run() {
    Result result = computeResult();
    storeResult(result);
  }
  private Result computeResult() {
    ...
  }
  private void storeResult(Result result) {
    ...
  }
}

There may be minor refactoring around the storage of result, depending on how that is implemented, but the named class provides more options to encapsulate the property as well. Note that even for this skeletal task, factoring out the anonymous inner class greatly improves the readability of the parallelCompute() method.

It is a trivial refactoring to extract the named inner class ComputeRunnable to a separate class file. From there we can use our other established techniques to test the actual functionality of the class.

But we made a significant assumption here that we said is often but not always true: that the task does not really require thread synchronization to do its job. The remainder of the chapter will introduce techniques for those scenarios.

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

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