Component Models


One of the standard definitions for a component is given by Clemens Szyperski [Szyp98]:

A software component is a unit of composition with contractually specified interfaces and explicit context dependencies only. A software component can be deployed independently and is subject to composition by third parties.

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 Components

We 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 Model

Enterprise 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.

The BeanBox TestBox

The BeanBox works particularly well as a test environment for those beans that are visual. The tester can use the box to interactively cause events to be generated and then observe the result. It is also possible to specialize the BeanBox and execute a set of tests repeatedly. Even for beans that are not visual, the BeanBox can be a useful test environment with a few simple additions. A simple method is added for each event to provide a visible cue that the bean has received and reacted to the event. This works because there is a well-defined set of events that beans can respond to.

For other types of components, we have extended the BeanBox approach to what we are calling the TestBox. Each TestBox is implemented to respond to a specific set of methods that are described in an interface definition. For every interface that will be widely used, the creation of a TestBox is a worthwhile investment. We have integrated the TestBox concept with the parallel architecture for component testing (PACT) test class approach. Each test class contains test cases written to invoke methods on the TestBox.

Testing Components versus Objects

From a testing perspective, there are many similarities and a few differences between objects and components. The similarities include the following:

  1. well-defined specification The technologies used to encapsulate functionality into a component also support the definition of a specification. Specifications written in standard notations such as object constraint language (OCL) facilitate writing test cases. The specification for a component is an aggregation of the specifications of all of the services provided by the component. This is accompanied by an invariant that constrains the overall state of the component. The preconditions for individual services are supplemented with a specification of the "requires" interface of the component. The requires interface specifies the external behaviors that the component must have access to in order to perform correctly. See Chapter 5 for details of building tests from pre- and postconditions and invariants.

  2. dynamic plug and play Component technologies allow for "hot swapping" of components within an application. This is realized using the polymorphic substitution principle. The interfaces implemented by the component are the "types" that are related in a generalization hierarchy and are used to determine valid substitutions. The orthogonal array testing system (OATS) statistically guided "experiments" that test a subset of the possible configurations of components is useful in achieving thorough test coverage with the minimum number of tests. See Chapter 7 for more about sampling among polymorphically interchangeable units.

  3. standard patterns of interaction The design and development techniques used for components follow a pattern-oriented development approach. The use of the test patterns approach will be of benefit here. See Chapter 8 and later in this chapter for examples of test patterns.

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:

  1. Components are clusters of objects. Testing components has many of the elements of testing the interactions among a cluster of objects.

  2. A component is larger than a single class. It is more difficult to get good code coverage using a functional approach based on the component specification.

  3. A component is intended to be more autonomous than an object. The packaging for a component will usually satisfy all of its "requires" specification. That is, the component will usually be shipped with the resources, libraries, graphics, and data that it needs to work. This can present problems if resources such as dynamic link libraries (DLLs) needed by the component and packaged with it are visible and conflict with existing versions of the same DLLs used by other components. In Testing the System Deployment on page 327 and Testing after Deployment on page 328, we talked about techniques that can be employed at the component level in the same way that they are used at the system level.

Component Test Processes

We will focus on three test processes that involve components at various points in their life cycle.

  1. A component is tested as it is developed.

  2. Components are tested as they are integrated into a larger aggregate.

  3. A component is tested prior to being selected for use.

In each of these processes we are interested in certain features of the component that might trigger a failure.

Development-Level Component Test Process

The development process includes specifying, developing, and testing a component as an isolated entity. Some of the defect triggers include the following:

  1. Interactions A component will typically be an aggregate of several objects. An error in one object may cause errors to ripple through many of the objects in the component. Use the techniques in Chapter 6 to define test cases that exercise the interclass interactions between the objects within the component.

  2. Concurrency Most components will encapsulate multiple threads. Use the techniques discussed in Chapter 8 to test for interactions among the threads. Use PACT classes that incorporate multiple threads to simultaneously invoke methods on the component's interface.

Integration-Level Component Test Process

The integration of a set of components involves at least three special features that may trigger failures.

  1. Sequence The protocol previously mentioned is the sequence of messages exchanged between two components. Test cases should be constructed to cover each protocol in which the components participate. This includes throwing exceptions between components.

  2. Timing Race conditions can exist when two components that manage their own threads are integrated. Messages may not be sent or arrive when expected. Test cases should investigate the effects of exaggerated latencies on the interaction between two components. (It may be that your in-house network provides this as a feature.)

  3. Communication Components that are created in different models, such as CORBA and COM, communicate through adapters that are referred to as gateways or bridges. There are also mappings from the primitive data types in a specific programming language to the representation in the component model. Test cases should be constructed that traverse each of the communication paths in the program.

Acceptance-Level Component Test Process

When 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:

  1. Check operation at the extreme points on each service's specification. Also determine how defensive the component is by providing values outside those extreme points.

  2. Check compatibility with nonstandard portions of the infrastructure. If the component is based on the CORBA infrastructure, determine that the component makes appropriate calls to connect to and use the ORB since there is not a standard API.

  3. Load test the component. Simple test programs may fail to reveal a very inefficient algorithm or an abnormally large memory requirement. At least use representative amounts of data that simulate real use and, if there is time, stress the system beyond the usual limits to determine its robustness.

Test Cases Based on Interfaces

The 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.

TestBox versus Test Class

The TestBox provides an execution environment for the component under test. It provides special services that are specific to a special type of component. For example, the CORBA TestBox provides access to the object request broker (ORB) and other services. A test class is the aggregation of all the tests for a component. A TestBox uses a test class to provide test cases. A TestBox can work with many different components and their corresponding test classes.

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 protocol

graphics/10fig01.gif

The 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 Component

In 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 change

graphics/10fig02.gif

In 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 bean

graphics/10fig03.gif

Figure 10.4. A vetoable-change test pattern

graphics/10fig04.gif

graphics/10fig04b.gif

Figure 10.5. A class diagram for a vetoable-change test pattern

graphics/10fig05.gif

Figure 10.6. An activity diagram for the vetoable-change pattern

graphics/10fig06.gif

Figure 10.7. A Java reflective test case

graphics/10fig07.gif

Figure 10.8. A component test plan for the GameBoard component

graphics/10fig08.gif

If 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.



A Practical Guide to Testing Object-Oriented Software
A Practical Guide to Testing Object-Oriented Software
ISBN: 0201325640
EAN: 2147483647
Year: 2005
Pages: 126

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net