As we present testing techniques for distributed components, we will generalize about the architecture and infrastructure for the system. Our specific examples will come from CORBA. We have even abandoned the terms "client" and "server" because they tend to have very rigid architectural connotations for some people. In a distributed object system, any provider of service will almost invariably also be requesting service from some other object.
In Figure 8.3 we illustrate the basic architecture of a distributed system. The major action occurs when the service requester sends a message to the service provider. That is certainly the intent of each test case. The request is first sent to the surrogate object that is local to the requester so the requester does not handle any of the distribution semantics. The surrogate contacts the communication infrastructure and passes on the request. The communication infrastructure may actually have to instantiate the service provider, but it eventually obtains a reference to the provider from an object locator service and passes along the request. The request may be channeled through a requester surrogate so that the provider is also protected from the details of distribution. The return, if any, follows the route back.
Figure 8.3. Generic architecture
At this level basically all three models are the same, although DCOM would return the result directly to the requester. As we discuss the components of the architecture, remember that an object can be, and often is, both a requester and a provider.
The requester participates in the distributed system as a stimulus. As such, its behaviors have been previously tested using the class testing techniques that we have already discussed with one exception: timing. If the requester sends any asynchronous messages (one-way messages in CORBA), the test cases must investigate the effect of the length of the time it takes to receive a reply. That is, when an asynchronous message is sent, the sender immediately proceeds to other business. The implementation of the sender may be written to expect an answer to the message within a certain amount of computation, but the implementation may not be properly written to wait for that answer if it is not received in the expected time. Test cases should be written to test this interaction under various load conditions, thereby introducing different amounts of latency (delay) in the communication.
The requester also participates in the interaction tests once the provider has been class tested using the specialized techniques discussed in the next section. The focus of these tests is the protocol between the requester and providers. Remember from Chapter 6 that the protocol describes the complete set of messages sent between two objects to accomplish an identifiable task. This is a separate phase of testing because there are often multiple providers of the same service. The protocol test suite and the individual test cases can be reused every time a new provider for a given protocol is added to inventory. The protocol test suite provides a life-cycle approach to testing the interactions.
The provider is the central figure in a distributed interaction. It performs behaviors and, in some cases, returns information to the requester. The complete interface of the provider can be tested using the basic class testing techniques discussed previously. Those behaviors that are expected to be invoked by other distributed objects will require specialized testing that we will describe in the next section.
The provider is registered with the infrastructure along with information about the services that it provides. In some cases, the provider may not be an object waiting actively in memory for a request to be received. It is first instantiated, and then the request is forwarded to it. This can be the source of timing differences. Any provider that can be dynamically instantiated upon request should be exercised using test cases starting both from instantiated and noninstantiated scenarios.
Stubs and Skeletons
A stub is the surrogate for the provider in the requester process. A skeleton is the surrogate on the requester side. The stub keeps the requester from knowing about the semantics of the infrastructure. Some implementations of these infrastructures are intelligent enough to reconfigure themselves depending on whether the two objects are actually in the same process, in different processes on the same machine, or on different machines with different architectures.
As it reconfigures itself, the infrastructure will add or remove stubs and skeletons or other method calls. This changes the path through which a request must travel. Interaction test suites should be designed to execute a set of tests over the path corresponding to each possible configuration.
Local and Remote Interfaces
The interface of a distributed object is often divided into local and remote interfaces. The remote interface is the specification of those services that may be requested by an object that is outside of the process in which the provider is located. Those behaviors specified in the local interface can only be accessed by objects in the same process.
The local interface can be tested using the usual class testing techniques that we have already discussed. The remote interface can be tested by local test harnesses as an initial step but additional testing requiring a specialized environment is still needed.