Note: This email is primarily for students in 312, but it should be of
interest to all DrJava developers.
Eliot brought up a good question in today's lab session, while looking at a
test case that involved multithreaded code. (Actually, it was testing
calls between two different JVMs, which is an extreme case of multithreaded
code, but which happens often in DrJava.) It made me realize I should
explain the technique we use to test such behavior, as you guys are
starting to write your unit tests.
(Note: if you have never worked with multithreaded code before, you'll
want to see me for a crash course, because it shows up a *lot* in DrJava!)
The Situation -----
Suppose you have the following classes that you're testing:
class Foo {
Bar b = ...
void foo() { b.bar(); }
}
interface FooListener {
void done();
}
class Bar {
FooListener fl = ...
void bar() { ...; fl.done(); }
}
These classes run in different threads or JVMs. Essentially, Foo tells Bar
to do some work (perhaps over RMI), and Bar lets FooListener know when it's
done.
How To Test -----
We use a synchronization block in our test to help us wait for Bar to call
done(), since we don't know exactly when it's going to happen. We will
create a special FooListener for this purpose.
public class FooBarTest extends TestCase {
Foo f = ...; Bar b = ...;
public void testFooBar() {
// Create a listener for our test
FooListener fl = new FooListener() {
void done() {
synchronized(this) {
...
this.notify();
}
}
};
b.addListener(fl);
synchronized(fl) {
f.foo();
fl.wait();
}
b.removeListener(fl);
}
}
Things To Notice:
In our test case, we create a FooListener that synchronizes on itself in
the done() method. We add that listener to our Bar, and then call f.foo()
in a block of code that is also synchronized on the listener. This means
that we've acquired the lock on the listener, so *nothing* can be executed
in our listener's done() method until we release that lock.
Note that we have *no idea* when done() will be called. The OS might
schedule the Bar code to run before we can call fl.wait(), so done() might
get called immediately. Or the OS might not schedule it for several
seconds, in which case we've already called wait().
That's why we need to have the lock. Even if done() gets called first, it
*can't do anything* until we release the lock by calling fl.wait(). Then
done() can finish at its own convenience, and notify the waiting test
method when it is finished.
This construct is used everywhere in DrJava's unit tests, so it is very
important that you understand it, to avoid creating timing bugs and
synchronization problems. If you haven't had any experience with
multithreaded code before, please make an appointment to come talk to me
and I can help walk you through it.
If you want to look at some of this code in practice, here are a few good
tests to browse:
edu.rice.cs.util.NewJVMTest
(uses a TestJVMExtension class rather than a listener)
edu.rice.cs.drjava.model.GlobalModelOtherTest
(waits/notifies on certain events on a GlobalModelListener)
Let me know if you have any questions, or if this isn't clear! I'll be
adding some of this to the developer documentation...
Charlie
|