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