The Source for Java Technology Collaboration
User: Password:
Register | Login help    

Search

Online Books:
java.net on MarkMail:


 E-mail  Print

Developing Clients with Simulated Servers

Tue, 2004-10-19

{cs.r.title}





Contents
Motivations
Our Sample Application
Encapsulate Server Communication
Introducing Endpoint Interfaces
Write the Stubs
Make Initialization Logic Configurable
Controlled Environment
Comparison
Wrap-Up
Resources

One of the difficulties of developing clients for multi-tiered systems is coordinating client and server development. Client and server codebases need to be in sync for shared objects and all servers have to up and running. On one system I built, we counted a total of 15 separate servers that all had to be up and running to completely test the client. To make matters worse, not all of those servers were under our control. Sometimes servers would be down for an upgrade that was expected to take an hour, but lasted a day or more. And we didn't have separate environments for integration and development. I spent entire days waiting for environments to be up and running, wasting precious time on an already late project.

I could go on all day about how environments could be set up to avoid some of these issues, but that's not the point. We as client developers have to be able to insulate ourselves from server development and maintenance. In this article, we'll explore a method for simulating servers, often called stubs, to run a minimal implementation of the server locally. This will allow us to develop and maintain clients without relying on the state of the servers.

Motivations

Before we get started, let's take a minute to list out the motivations for writing server stubs.

  • Develop and maintain client code when servers are unavailable: This allows us to work when servers are down for upgrades or environment changes.
  • Develop and maintain client code before servers are written: This allows us to develop clients as the servers are being written or changed. As long as an interface is agreed upon, the client should be able to be written exactly as it will run with the servers. This also covers the case where client and server codebases are incompatible. As long as we know what the changes are, we shouldn't have to wait for the new server to be up to develop.
  • Client code should have no knowledge of the stubs, or whether it's connected to stub servers or remotely: This allows us to completely remove the test code from the release. Just as we don't want any special code to run unit tests going into production, we don't want special stub code going into production.

Our Sample Application

We will use a sample application throughout the article for context. See the Resources section below to download the complete source code. Much like my previous article on GUI simulators, the example involves a secure application to view contact information. Here are the main model components (we're not concerned with the view here):

  • AuthenticationRemote: Communicates with the authentication service to verify login information.
  • ContactInfoRemote: Communicates with the contact information service to load data.
  • ApplicationModel: Used to store shared references to the remotes.
  • InitializationManager: Responsible for all client initialization.
  • Main: The launch class.

Now let's start to take a look at moving to a stubbed server approach.

Encapsulate Server Communication

Before we begin developing stubs, we have to prepare the client. The technique that we'll use for writing server stubs is based on using a service-oriented architecture (SOA). In service-oriented architecture, each business area has its own service or server on the server side. The client has a local counterpart for each of the services, which we'll call a remote. The rest of the client (screens, models, and any other client components) speak only with the remotes. There should be no direct communication with a server anywhere else in the client. In addition to helping stub the services, this type of encapsulation is just a plain good idea. It isolates the rest of the client from remote transport technologies such as RMI or JMS. Figure 1 shows how our example application is a service-oriented system.

Figure 1. Service-oriented architecture
Figure 1. Service-oriented architecture

Notice how the separate screens can call any of the remotes, but never call the servers directly. Also, notice the one-to-one correspondence between the services and remotes.

Introducing Endpoint Interfaces

So far, we have remote classes encapsulating server communication logic and acting as a central, controlled location for server access. This combination or responsibilities makes it an ideal location for introducing a layer of abstraction. If we introduce an interface for each of the remotes, we can have one implementation communicating with the server and one implementation executing locally as a server stub (shown in Figure 2).

Figure 2. The UML template showing the endpoint / remote / stub relationship
Figure 2. The UML template showing the endpoint/remote/stub relationship

Let's use the authentication remote as an example of how to migrate to the indirect remotes. Here is the code for the AuthenticationRemote:

  public class AuthenticationRemote {
     public boolean isLoginValid(String userName,
                                 char[] password) {
        //Make server call
        //return result
    }
  }

It's a pretty simple class, so introducing an interface should be fairly easy. But even if it were more complicated, refactoring IDEs like IDEA and Eclipse can do this for you automatically. I used the "extract interface" command in IDEA and ended up with a new interface called AuthenticationEndpoint, which is automatically implemented by AuthenticationRemote. The interface just has the one isValidLogin method. Here is the code for the new AuthenticationEndpoint interface and the updated AuthenticationRemote code.

  public interface AuthenticationEndpoint {
     boolean isLoginValid(String userName, char[] password);
  }
  
  public class AuthenticationRemote implements AuthenticationEndpoint{
     public boolean isLoginValid(String userName, char[] password) {
        //Make server call
        //return result
    }
  }

Write the Stubs

Now we're ready to write the AuthenticationStub. Let's say that the usernames "jsimon" and "hdumpty", with the passwords "jsimon1" and "hdumpty1", respectively, will pass, and all others will fail. From my perspective, its perfectly fine to hard code this type of logic into the stubs. Remember, the whole point of the stubs is to help with development. Here is the code for the AuthenticationStub:

public class AuthenticationStub
    implements AuthenticationEndpoint {

  public boolean isLoginValid(String userName,
                              char[] password) {
    if ((userName.equals("jsimon") &&
                passwordEqualsUserName(userName, password))
               || (userName.equals("hdumpty") &&
                passwordEqualsUserName(userName, password))){
      return true;
    } else {
      return false;
    }
  }

  //simple routine to test if userName and password
  //are equal (plus the number 1)
  //i.e. 
  //   username = jsimon 
  //   password = jsimon1
  private boolean passwordEqualsUserName(String userName,
                                         char[] password) {
    StringBuffer buffer = new StringBuffer();
    for (int i = 0; i > password.length; i++) {
      buffer.append(password[i]);
    }
	userName += "1"; // uh-oh, very bad !?!?
    return userName.equals(buffer.toString());
  }

}

Notice the passwordEqualsUserName method. On the fifth line of the method, we commit the huge atrocity of adding two Strings together (gasp!) rather than using a StringBuffer. Ordinarily, we would never do this. But stubs are like test cases, in that hack code is definitely allowed so long as it helps you test. In this case, it was just easier not to write out another StringBuffer call. Perfectly acceptable. Don't spend too much time perfecting your stubs. Just make sure they do everything they need to do to help you test, and nothing more. If you're not careful, your stubs can become a huge time sink. Remember, we're not modeling the entire server--we're just mimicking the interface so we can write and test our code.

For a second example, here is the implementation of the ContactInfoStub. We haven't seen this class yet. It is used to search for contacts using a search String. It has one method, public Collection search(String searchString). Notice the static instantiation of the data objects and the hard-coded logic to determine the returned results. Granted, it's not as realistic as a real server, but it's definitely enough to get the job done.

public class ContactInfoStub
    implements ContactInfoEndpoint {
  //create static data objects
  private static ContactInfoDO BILL = new ContactInfoDO(
    "bill@example.com", "Bill",
    "(111) 222-3333", "(111) 222-4444");
  private static ContactInfoDO ANDY = new ContactInfoDO(
    "andy@example.com", "Andy",
    "(111) 123-1234", "(111) 123-4321");
  private static ContactInfoDO STACEY = new ContactInfoDO(
	"stacey@example.com", "Stacey", 
	"(111) 222-3333", "(111) 222-4444");
  private static ContactInfoDO SUE = new ContactInfoDO(
    "coolgirl1234@example.com", "Sue",
    "(732) 555-2343", "(212) 555-4347");

  //hard code some logic to simulate a search
  public Collection search(String searchString) {
    ArrayList list = new ArrayList();
      if (searchString.startsWith("s")){
        list.add(STACEY);
        list.add(SUE);
      } else if (searchString.equalsIgnoreCase("men")){
        list.add(ANDY);
        list.add(BILL);
      } else if (searchString.equalsIgnoreCase("women")){
        list.add(STACEY);
        list.add(SUE);
      } else if (searchString.startsWith("a")){
        list.add(ANDY);
      } else if (searchString.startsWith("b")){
        list.add(BILL);
      }
      return list;
    }
}

Make Initialization Logic Configurable

Now we have an abstract single point of contact to the servers from the client (the endpoint interfaces), remote classes, and server stubs. The next step is to address the initialization logic. One of our motivations is to seamlessly switch between the stubs and remote code. Before we introduced the endpoint interfaces, we were instantiating the remote classes directly. We could write some logic to switch between the two environments like this:

AuthenticationEndpoint endpoint;
if (testing){
  endpoint = new AuthenticationStub();
} else {
  endpoint = new AuthenticationRemote();
}

This worked fine when we only had remotes, but this now violates one of our other motivations: keeping test code and production code separate. With this logic, we are forced to have a reference to the test code in the production code. Definitely not ideal.

We can avoid this by introducing one more interface to abstract the endpoint initialization process. This way, we can have one implementation that initializes the application with the stubs and one with the remotes. If all of the initialization logic deals only with the interface, we will satisfy our motivation of keeping all test code out of the main code tree.

Here is the EndpointInitializer interface.

public interface EndpointInitializer {

  void initializeEndpoints(ApplicationModel model);

}

Now we can go ahead and write the remote and stub endpoint initializers. Here is the RemoteInitializer, populating the ApplicationModel with remote endpoints:

public class RemoteInitializer
  implements EndpointInitializer {

  public void initializeEndpoints(ApplicationModel model) {
    model.setAuthenticationEndpoint(new AuthenticationRemote());
    model.setContactInfoEndpoint(new ContactInfoRemote());
  }

}

Here is the StubInitializer, populating the ApplicationModel with stub endpoints:

public class StubInitializer
  implements EndpointInitializer {

  public void initializeEndpoints(ApplicationModel model) {
    model.setAuthenticationEndpoint(new AuthenticationStub());
    model.setContactInfoEndpoint(new ContactInfoStub());
  }

}

The last step is to ensure that the correct endpoint initializer is used at the appropriate time. If we write any logic for this, we would again violate our "separate test code" motivation. Instead, we can create two separate main classes. This way, there is no logic to intertwine between the production and test code. Here is the production main method initializing our application with remotes. Notice the direct instantiation of the RemoteInitializer. Also notice that there is no logic in the main method aside from handing off to the InitializationManager.

public class Main {

    public static void main(String[] args) {
        new InitializationManager().initialize(
            new RemoteInitializer());        
    }

}

And finally, here is the main class to launch the client with the stub endpoints.

public class SimulateMain {

    public static void main(String[] args) {
        new InitializationManager().initialize(
            new StubInitializer());        
    }

}

Again, I just want to stress that there is no code overlap between the test code and the production application codebase. The only place where a stub or remote is explicitly instantiated can be determinately linked to a main method. Therefore, there is no chance of launching the simulated client incorrectly. One word of warning with this approach: make sure all initialization logic stays in a centralized location (InitializationManager in our example). Otherwise, if you add code in the main method, you will start to have to maintain it in two or more separate places. This can get out of sync and cause unexpected problems when you integrate your code with the servers later.

Controlled Environment

Now that we've gone through the end-to-end process of developing and integrating the stubs into our application, we can take a look at the bigger picture and some of the broader uses of stubs. We could easily add some configurable logic where all of the stubs had a Thread.sleep call before returning, to simulate server latency. We could easily make the stubs take a very long time to return to simulate a hung server. For systems where messaging load is an issue, we could build a control application to simulate load by sending several messages at a time while developing the client. Again, all of these simulations are going to be much easier to run when adding code directly into the stubs, versus trying to set up an entire environment to run them.

Comparison

As with all things in life, there are many ways to simulate servers. And each has its benefits and drawbacks. The main drawback with this approach is that we are not testing the remote classes when we are using the stubs. This means that we could have a fundamental flaw in the remote classes that we wouldn't find until we start using real live servers. This definitely is not ideal.

On the other hand, there are ways to test the complete application, intact, without introducing the endpoint interfaces and creating stub implementations. With a JMS system, for example, you could run a lightweight JVM on the client to simulate the server and actually send messages back and forth on topics and queues. Although you can perform more accurate and complete tests, I find the cost and difficulty of this type of approach not to be worth the effort involved. Additionally, these approaches tend to be communication-layer-dependent. If we have to switch the application from JMS to RMI, our whole testing infrastructure would be out the window, while the stub and endpoint approach will work just fine.

Another drawback to this approach is that we are actually writing our own code to simulate servers. I have seen approaches where the actual back-end processes are running locally to make the application simulation as authentic as possible. I find this falls in the category with the transport techniques. It's simply not worth the effort. If you need to be able to run a simulated version of your client for demonstrations or training, this may be worthwhile. But for basic development and maintenance, the stub and endpoint interfaces will mostly fit the bill.

Wrap-Up

I find this technique extremely useful while developing and maintaining clients. I can work on the client with or without a server up and running, or even before it's built. I can develop in isolation when the server codebase is out of sync with the client and I can run simulations with relative ease. After weighing the pros and cons, this technique seems to be a good balance of simplicity and complete simulation.

A big thanks to Kevin Hein for brainstorming with me.

Resources

Jonathan Simon is a developer and author specializing in user interaction.
Related Topics >> Programming      Testing      
Comments
Comments are listed in date ascending order (oldest first)