1. Field of the Invention
The present invention relates generally to data processing systems. More specifically, the present invention relates to a computer implemented method, computer program product and data processing system for binding service component implementations for specific unit test cases.
2. Description of the Related Art
The most advanced software architectures of today are service oriented, where an enterprise application is composed of one or more independent modules called components, each of which exposes one or more services. Each service is comprised of one or more operations, which, when invoked, perform a specified function. For example, a service may have operations that perform data retrieval, update and validation.
Specifying a service on a component effectively defines an interface in some target language. One language may be used by the user of the service, the client application; while another language may be used by the provider of the service, the implementation. In the course of implementing a service, it is very likely that the service will use other services in its implementation. This establishes a dependency between services that is exactly equivalent to any other client.
An important benefit of a service-oriented architecture is that components can be developed in parallel and reused by many clients including other services in the system. But a significant tradeoff comes during unit testing. Unit testing means verifying that a given service is correct by testing the given service in isolation. Whenever a service is unit tested, the services upon which the component under test depends may not be available. The depended upon services may not be available either because the code for the depended upon service is being developed in parallel to the service being tested, or because loading the depended upon service into the test environment is deemed too expensive.
A component under test with such dependencies on other services is usually bound to an implementation that simulates the depended upon service. A simulated service is often called a “stub” because the service is comprised of operations implemented only enough to allow specific pathways through the code in the component under test to be exercised to insure correctness. However, writing stub code for a depended upon service can be almost as complex as writing the full service implementation.
One reason for this complexity is that the type of output returned by the operations in the depended upon services can vary widely. For example, some operations are “one way” or “output only” functions that do not return a value. As an example, methods that return “void” in Java are one-way services. A void return is a return where nothing was returned. A void is different than a null. A null return is where a data object is returned but the data object is empty. Other operations return a “primitive” type. A primitive data type is a data type that can have only one value at a time and is the simplest built-in form of data, such as an integer, floating point number, or character string. Still others return “data transfer objects.” Data transfer objects are complex structures of static data values that can contain both primitives and other data transfer objects. Other operations return objects that may include not just static data values, but also computed values and dynamic connections to system resources that must be initialized through programmatic means at run-time.
Further, some operations return fixed or variable length arrays, meaning zero or more instances, of primitives, structures, or dynamic objects. An array is a collection of one or more data objects, called elements, of the same data type which can be selected by an index belonging to the index subtype of the array.
Another reason for the complexity of implementing a stub is that the number and type of input parameters to a given service operation can vary in a similar manner. Some operations take no parameters, while others take one or more primitive or complex values as input. For example, a service operation performing a read may take a simple integer that represents the unique identifier, known as the “key”, of the record. Similarly, an update operation may take a data transfer object that includes both the key and the associated data values.
Yet another reason for the complexity is that many operations differentiate between the normal return of the operation and exceptional conditions that return a different type of result, as described above, for each condition. An exceptional condition is a result other than the normal return. There are two types of exceptions, expected and unexpected. An unexpected (or “system”) exception is a problem that prevents the continuation of the method or scope that is executing. For example, a service operation that depends on a database cannot continue execution if the database is down. An expected (or “application”) exception is not the normal result of the operation, but is one that can be expected as a possible return. For example, an operation to submit an order may “throw” an application exception if the user does not exist, the order is not in the open state, or if the order has no line items. To throw an exception is to cause the exception to be generated.
Still another reason for the complexity in implementing stub implementations is that some operations, especially those that do not return a value, have a “side effect.” A side effect is something which changes the state of the system and that can be discovered by invoking another service or operation. For example, a service operation that inserts a record with a given unique identifier into a database (the side effect) may not return a value as a result. A subsequent call to a service operation that finds the record with the unique identifier will return the record, providing evidence that the insert operation was invoked previously. If the component under test should call these depended upon services in a different order, such as reading before inserting, different values (such as a null return) or exceptional conditions (such as an expected exception, ‘the key was not found’) may be returned.
Taken together, the complexity of a stub implementation of a depended upon service is that the stub implementation must be able to map the input and current state of the system to the normal or exceptional output such that the stub implementation matches the expectations of the test cases.
The practical result of the benefits of a component architecture and the complexity of stub implementations is that the relationship of a component under test and the depended upon services is always a dynamic one. In the early stages of testing, usually neither the implementation of the depended upon service nor the expectations on a given stub implementation are ready. In this phase, testing is usually manually executed one test case at a time. Sometimes the test cases are executed in an exploratory mode to see which code paths are exercised by given returns from depended upon services.
Later, as the unit test cases get organized into test suites designed to exercise as many paths through the code as necessary to verify correctness, the expectations of the stub implementations become well defined. In this phase, creating a stub implementation such that testing can be automated so that the testing is quickly repeatable, is possible and desirable. And later still, as one or more depended upon services are completed, running the same test suite with the real implementations of one or more depended upon services to verify that the components work together becomes desirable. This latter form of testing is sometimes called integration testing because multiple components are integrated into the same application under test.
To support this kind of dynamic approach to testing, some means are needed to enable a component to be tested without regard to the status of other components, even the stub implementations themselves. The question arises as to how details of calls into a dynamically bound implementation are captured, how the returned values are bound, and how side effects are captured, such that they meet the expectations of the code under test.