One of the standard definitions for a component is given by Clemens Szyperski [Szyp98]:
This addresses both the technical and market aspects of "component." Even though we will continue to use our less formal definition, we will use this definition to organize our discussion of testing components. A component is a "chunk" of functionality hidden within some type of package but advertised through one or more interfaces. The complete specification for a component is the list of services (we will use the term service in place of method in the context of a component) available to users of the component. Each service is defined by a precondition and a postcondition just as we have described in earlier chapters. So creating functional test cases for a component is no different than creating them for a class except that each service is usually a larger chunk than a method and has more parameters. This, in turn, means more permutations of values. A component usually implements several interfaces and each interface may be implemented by several components. Distributed ComponentsWe have already discussed this topic in Chapter 8 because the infrastructure needed for component interoperability can, with only a little effort, be extended to support the distribution of components. We also discussed aspects of this topic in Chapter 9 from the "system" perspective. Here we will focus on the "component" aspect of distributed objects. Distributed environments were the first products to provide sufficient infrastructure to separate the functionality of a unit from its ability to communicate with other units. These systems provide an extensive set of services that "glue" together components based on a set of assumptions and a set of interfaces. Some of these infrastructures support the interoperating of components that are writing in different languages, executing on machines of different architectures, or residing in a different operating environment. The major component models include the Object Management Group's (OMG) Common Object Request Broker Architecture (CORBA) specification [OMG98], Microsoft's Distributed Common Object Model (DCOM) [Redm97], and Sun's Enterprise JavaBeans (EJB) Model [MoHa00]. We have already described CORBA and DCOM in Chapter 8 so we will focus on EJB here. Enterprise JavaBeans Component ModelEnterprise JavaBeans are first of all Java beans. A bean is a cluster of one or more classes that cooperate to provide the behavior described in the bean's specification. The "bean" model relies on a developer's adherence to specific design patterns rather than requiring that every component implements a single standard interface or inherits from a common base class. The bean component model provides a standard approach to how components interact with each other and how they are comprised into applications. The bean model adds a level of development between the design and implementation of the classes and its instantiation within an application. Each bean defines a set of properties. A property is an attribute that can take on one of a set of values. For example, a bean might select one object from a pool of available objects because that object implements a specific sorting algorithm. The values of the properties for a bean are stored in a separate properties file. Using a development tool, a developer can specialize a bean object, as opposed to a class, by giving its attributes specific values. These specialized beans should be tested to be certain the property values are behaving properly. A bean interacts with other beans through events and standard patterns of interaction. Each type of bean specifies a set of events that it will generate and a set to which it will respond. The BeanBox [White97] is a simple bean that allows developers to instantiate and exercise their beans by sending standard events to the instance in the box and observing the response. The sidebar The BeanBox TestBox explores this further. Enterprise JavaBeans are beans that participate in a distributed interaction with other beans using the Remote Method Invocation (RMI) protocol that was briefly described in the RMI section on page 284. These beans often also use a Web-based user interface and an application server. Probably the most important point about beans from a testing perspective is the emphasis on patterns. Two of the most frequently used patterns in bean design are the Listener pattern and the Vetoable-change pattern. In the listener pattern, a mechanism similar to event registration and broadcast is established.
Testing Components versus ObjectsFrom a testing perspective, there are many similarities and a few differences between objects and components. The similarities include the following:
There are some differences between components and objects. These are usually related to either the scope of the functionality or the technology used. They include the following:
Component Test ProcessesWe will focus on three test processes that involve components at various points in their life cycle.
In each of these processes we are interested in certain features of the component that might trigger a failure. Development-Level Component Test ProcessThe development process includes specifying, developing, and testing a component as an isolated entity. Some of the defect triggers include the following:
Integration-Level Component Test ProcessThe integration of a set of components involves at least three special features that may trigger failures.
Acceptance-Level Component Test ProcessWhen components are obtained from other commercial sources, or even freeware if you dare, they should be carefully tested to measure their quality. Although there is probably a need for a comprehensive set of tests, the following areas are particularly important:
Test Cases Based on InterfacesThe PACT approach applies to interfaces just as it does to classes. That is, each interface should be accompanied by a test class that defines a set of functional test cases for the services listed in the interface. The test class for each implementer of the interface aggregates the test cases defined for the interface. There are two reasons why the relationship between test classes is aggregation rather than inheritance. First, many languages do not support multiple inheritance yet often multiple interfaces apply to a single class. Second, an interface does not provide an implementation, but only a specification. The test class serves as a proxy and passes through requests for tests to an instance of the interface test class. This reuse of tests from interfaces is even more productive for "standard" interfaces. A number of international, commercial, and ad-hoc standards such as CORBA and ODBC are being used in component-based systems. Test capabilities developed against the interface of the ORB adopted by a company should be made available to the wider development community of the company. A TestBox is created for each special type of component and used in conjunction with the test classes (see TestBox versus Test Class above). This communication of standard test cases is a service that can be provided by the quality assurance group of the company. Providing detailed tests of these standard tasks is particularly useful because many of them involve asynchronous interactions between threads.
The packaging technology for a component is used to encapsulate all of the pieces that comprise the component. DLLs and Java Archives (JAR) are two widely used packaging technologies. During the production and creation of the packages, it is important that test cases be run against the product so that every package is used at least once. This is particularly important with the dynamic JAR files and other dynamic resources such as Web pages or scripts. The protocol between two components is the sequence of messages exchanged between them. This is a further constraint on the two components. Not only should a component advertise the services that it provides (through its interface) and what it requires (through its preconditions), but it should also describe the sequence in which these interactions are expected. By combining the provided and required specifications of the two components, the overall sequence is defined. One set of test cases defined from the protocols should exercise the integration of two components through the complete protocol. Consider the interaction between two components when one controls a piece of hardware and the other interacts with the user, as in Figure 10.1. The user component wants the hardware to perform a service that will take a few seconds, but the user component does not want to block it during that period. Figure 10.1. A protocolThe user might want to cancel the operation so the user component must be free to receive and process events. The user component sends an asynchronous message to the hardware component to start the action. At some point, the hardware component that services multiple clients sends an asynchronous message back that the action has been started. A similar exchange occurs when the user component wants the hardware to stop. These four messages occur as a group. A designer incorporating these components into their design need to understand this grouping for which there is no notational convention. Two of the messages are for services on one component and two are for the other component. A component may participate in several protocols by having sets of its methods included in each protocol definition. A service provided by a component may be included in multiple protocol definitions. The component's test suite should include test cases in which the Tester class plays the role of the other component in a protocol. This means that in Java, for example, the Tester class may implement several interfaces. This allows a Tester object to pass itself as a parameter to the component under test, and then to invoke the services specified in the protocol. Case Study A GameBoard ComponentIn the Java version of the tic-tac-toe system that we developed, the game board is implemented as a Java bean called GameBoard. Among other things, this means that the game board is written to be much more general than just the user interface for a tic-tac-toe game. It is written to be configured as the game board for any game that requires a rectangular grid of positions. It also means that the "component" comprises several classes. There is a GameBoardInfo class that is a standard feature of beans and a GameBoardPosition class that abstracts the concept of each location on the board. The GameBoard bean is designed following both standard Java and JavaBeans design patterns. The ability to select a square on the tic-tac-toe game board is implemented as a vetoable change. In this design pattern, objects register to receive notification of a change and they have the opportunity to abort, or veto, the change. If the change is not vetoed, it is made. If it is vetoed, then the change is not made. Figure 10.2 illustrates the vetoable-change algorithm. Figure 10.2. An activity diagram for a vetoable changeIn Figure 10.3 we provide the interface for the GameBoard bean. It includes the signature of the setMove(int) method. This method invokes the vetoable-change mechanism. The test pattern for this design pattern is described in Figure 10.4 and is implemented by the classes shown in Figure 10.5. The flow of actions in using the test pattern is shown in Figure 10.6. In Figure 10.7 we show an implementation of the vetoable-change test pattern as programmed in the setMoveTest() method. Figure 10.8 shows a test plan for the component. Figure 10.3. An interface for a GameBoard beanFigure 10.4. A vetoable-change test patternFigure 10.5. A class diagram for a vetoable-change test patternFigure 10.6. An activity diagram for the vetoable-change patternFigure 10.7. A Java reflective test caseFigure 10.8. A component test plan for the GameBoard componentIf there were templated methods in Java, as in C++, we could generate a test method that could be (re)used across a number of methods and across a number of classes. The method allows the tester to test each of the game positions on the tic-tac-toe game board. Since the test method is written using the Java reflective API, it can also be cut and pasted to other test classes and easily modified. The vetoable-change design pattern is used very often in JavaBeans. In fact, it is usually used multiple times within a single visible bean. The test pattern given in Figure 10.4 can be applied multiple times in the same test class. |