JaVa
   

RMI

RMI consists basically of two parts: an interface to address remote objects and an implementation of this interface. To deploy this implementation, JDK offers some additional helper classes, a tool to create local stub objects (rmic), and a name service to register objects (rmiregistry). The following discussion assumes a basic knowledge of RMI, found in the literature [Darwin01]. The following subsections show how to test-first develop a simple RMI server and the pertaining client.

The Server

The interface of our example server is very simple:

public interface MyRemoteServer {
 String callService();
}



For now, everything runs locally, so we write the first test without anticipating any remote invocation:

public class MyRemoteServerTest extends TestCase {
 public void testCallService() {
 MyRemoteServer server = new MyRemoteServerImpl();
 assertEquals("OK", server.callService());
 }
}


The next steps try to push toward a soft transition to a distributed system. First, we will deal with one of RMI's testability shortcomings—the static naming interface. We move the part of the interface we are interested in from java.rmi.Naming to an interface:

import java.rmi.*;
import java.net.*;
public interface MyNaming {
 void rebind(String name, Remote obj)
 throws RemoteException, MalformedURLException;
 void unbind(String name) throws RemoteException,
 NotBoundException, MalformedURLException;
}


And for now, we offer a mock implementation instead: [3]

import java.rmi.*;
public class MockNaming implements MyNaming {
 public void rebind(String name, Remote obj) {...}
 public void unbind(String name) {...}
 public void expectRebind(String name, Class remoteType) {...}
 public void expectUnbind(String name) {...}
 public void verify() {...}
}


Back to the server. To be able to address the server remotely, we have to adapt its interface to the RMI practices and define a name for the RMI binding:

import java.rmi.*;
public interface MyRemoteServer extends Remote {
 String LOOKUP_NAME = "MyRemoteServer";
 String callService() throws RemoteException;
}


We are now ready to rebuild the testing method, taking the new interface, MyNaming, into account. In this context, we also have to bear in mind that a static method should be used to start the server; we call it create-Server() in this example:

public void testCallService() throws Exception {
 MockNaming mockNaming = new MockNaming();
 MyRemoteServer server =
 MyRemoteServerImpl.createServer(mockNaming);
 assertEquals("OK", server.callService());
 mockNaming.verify();
}


So far, the Naming object is passed as parameter, but not used yet. So let's extend our test:

public void testCallService() throws Exception {
 MockNaming mockNaming = new MockNaming();
 mockNaming.expectRebind(
 MyRemoteServer.LOOKUP_NAME, MyRemoteServerImpl.class);
 MyRemoteServer server =
 MyRemoteServerImpl.createServer(mockNaming);
 assertEquals("OK", server.callService());
 mockNaming.verify();
}


Next, we add a test that checks for correct release of the server, while doing a refactoring at the same time:

public class MyRemoteServerTest extends TestCase {
 private MyRemoteServer server;
 private MockNaming mockNaming;
 protected void setUp() throws Exception {
 mockNaming = new MockNaming();
 }
 public void testCallService() throws Exception {
 mockNaming.expectRebind(MyRemoteServer.LOOKUP_NAME,
 MyRemoteServerImpl.class);
 server = MyRemoteServerImpl.createServer(mockNaming);
 assertEquals("OK", server.callService());
 mockNaming.verify();
 }
 public void testReleaseService() throws Exception {
 mockNaming.expectRebind(MyRemoteServer.LOOKUP_NAME,
 MyRemoteServerImpl.class);
 mockNaming.expectUnbind(MyRemoteServer.LOOKUP_NAME);
 server = MyRemoteServerImpl.createServer(mockNaming);
 ((MyRemoteServerImpl) server).release();
 mockNaming.verify();
 }
}


What's missing? A test with the real RMI. Let's first consider the MyNaming implementation by use of java.rmi.Naming:

import java.rmi.*;
import java.net.*;
public class RMINaming implements MyNaming {
 public void rebind(String name, Remote obj)
 throws RemoteException, MalformedURLException {
 Naming.rebind(name, obj);
 }
 public void unbind(String name) throws RemoteException,
 NotBoundException, MalformedURLException {
 Naming.unbind(name);
 }
}



And here is the test case for use of RMI:

public void testRealService() throws Exception {
 RMINaming naming = new RMINaming();
 server = MyRemoteServerImpl.createServer(naming);
 MyRemoteServer client = (MyRemoteServer) Naming.lookup(
 MyRemoteServer.LOOKUP_NAME);
 assertEquals("OK", client.callService());
 ((MyRemoteServerImpl) server).release();
}


The following implementation resulted from the three test cases just implemented:

import java.net.*;
import java.rmi.*;
import java.rmi.server.*;
public class MyRemoteServerImpl extends UnicastRemoteObject implements MyRemoteServer {
 private MyNaming naming;
 private MyRemoteServerImpl(MyNaming naming)
 throws RemoteException {
 super();
 this.naming = naming;
 }
 public String callService() {
 return "OK";
 }
 public static MyRemoteServer createServer(MyNaming naming)
 throws RemoteException, MalformedURLException {
 MyRemoteServer server = new MyRemoteServerImpl(naming);
 naming.rebind(LOOKUP_NAME, server);
 return server;
 }
 public void release() throws RemoteException,
 NotBoundException, MalformedURLException {
 naming.unbind(LOOKUP_NAME);
 }
}


To give the test suite a chance, two things must be in place: the corresponding stub for MyRemoteServerImpl must have been created (e.g., by using rmic), and the RMI registry must be running (e.g., by starting it with rmiregistry from the command line). The attentive reader will now be able to easily imagine how MockNaming can be extended to test for the expected behavior of the createServer() method in case of errors.

The Client

Testing the "remote client" involves two tasks:

To complete the first task, we add a lookup() method to the interface MyNaming introduced above:

import java.rmi.*;
import java.net.*;
public interface MyNaming {
 ...
 Remote lookup(String name) throws NotBoundException,
 MalformedURLException, RemoteException;
}


Then we expand MockNaming accordingly:

import java.rmi.*;
public class MockNaming implements MyNaming {
 ...
 public void expectLookup(String name, Remote lookup) {...}
 public Remote lookup(String name) {...}
}


With this extended material on hand, we can now proceed with the first test for the client:

public class MyRemoteClientTest extends TestCase {
 public void testLookup() throws Exception {
 MyRemoteServer remote = new MyRemoteServer() {
 public String callService() {
 return "";
 }
 };
 MockNaming namingClient = new MockNaming();
 namingClient.expectLookup(
 MyRemoteServer.LOOKUP_NAME, remote);
 MyRemoteClient client = new MyRemoteClient(namingClient);
 namingClient.verify();
 }
}


As a substitutional stub object, the test uses an anonymous instance of the MyRemoteServer interface. This technique can also be used to test the actual client behavior. The example below tests a simple callTwice() method, and includes refactoring of the test class:

public class MyRemoteClientTest extends TestCase {
 private MockNaming naming;
 protected void setUp() {
 naming = new MockNaming();
 }
 public void testLookup() throws Exception {...}
 public void testCallTwice() throws Exception {
 MyRemoteServer remote1 = this.createRemoteServer("Test");
 naming.expectLookup(
 MyRemoteServer.LOOKUP_NAME, remote1);
 MyRemoteClient client1 = new MyRemoteClient(naming);
 assertEquals("TestTest", client1.callTwice());
 MyRemoteServer remote2 = this.createRemoteServer("Xyz");
 naming.expectLookup(
 MyRemoteServer.LOOKUP_NAME, remote2);
 MyRemoteClient client2 = new MyRemoteClient(naming);
 assertEquals("XyzXyz", client2.callTwice());
 }
 private MyRemoteServer createRemoteServer(
 final String returnString) {
 return new MyRemoteServer() {
 public String callService() {
 return returnString;
 }
 };
 }
}


Note that two different servers are used here to avoid a trivial implementation of the client class. Of course, we could use a mock implementation for MyRemoteServer as an alternative to anonymous internal instances. Such a mock class could nicely use all of its strengths when we also want to test for error handling or to extend the interface. Other tests have to deal with the client's behavior in the event of distribution problems. The following piece of code is an example, in that the client's reaction in case the server object is not registered:

public void testFailingLookup() throws Exception {
 naming.expectLookupThrowException(
 MyRemoteServer.LOOKUP_NAME, new NotBoundException("test"));
 try {
 MyRemoteClient client = new MyRemoteClient(naming);
 fail("NotBoundException expected");
 } catch (NotBoundException expected) {}
}



Suppose we had to deal with the simplest possible error handling mechanism in this case: NotBoundException is passed to the caller. For this test case, we had to extend the class MockNaming by the method expect-LookupThrowException(...). Similarly, we can test the client's behavior in the event the connection is interrupted. For this purpose, the server dummy must throw a RemoteException when callService() is invoked. Finally, we want to test the client's interaction with a real server object:

public void testWithRealServer() throws Exception {
 MyNaming naming = new RMINaming();
 MyRemoteServer server = MyRemoteServerImpl.createServer(naming);
 MyRemoteClient client = new MyRemoteClient(naming);
 assertEquals("OKOK", client.callTwice());
 ((MyRemoteServerImpl) server).release();
}


Similar to the server test suite, this test case requires that the stubs for MyRemoteServer have been created and an RMI registry has been started.

Summary of RMI

Let's take a close look at what we actually ensured with our previous test cases. The following aspects were tested:

However, our tests do not cover deployment problems in the network, including registry availability, avoiding name conflicts, setting the correct security manager, publishing stub classes on the Web, and many other aspects. These things should be covered in acceptance tests or specialized deployment tests. Note that the RMI invocation within the same JVM is sufficient for unit tests.

Another thing our previous example does not consider is test cases dealing with potential concurrency of remote method invocations. The good news is that this aspect is basically similar to the concurrency in local multi-thread apps (see ). This is because the actual synchronization happens between the client and the server stub, or between the client skeleton and the server implementation, residing in the same address space. [3]This implementation is available at the Web site to this tutorial.


JaVa
   
Comments