Friday, November 13, 2009

15.2 Issues in Testing Object-Oriented Software













15.2 Issues in Testing Object-Oriented Software


The characteristics of object-oriented software that impact test design are summarized in the sidebar on page 273 and discussed in more detail below.


The behavior of object-oriented programs is inherently stateful: The behavior of state-dependent behavior a method depends not only on the parameters passed explicitly to the method, but also on the state of the object. For example, method CheckConfiguration() of class Model, shown in Figure 15.1, returns True or False depending on whether all components are bound to compatible slots in the current object state.












1 public class Model extends Orders.CompositeItem {
2 public String modelID; // Database key for slots
3 private int baseWeight; // Weight excluding optional components
4 private int heightCm, widthCm, depthCm; // Dimensions if boxed
5 private Slot[] slots; // Component slots
6
7 private boolean legalConfig = false; // memoized result of isLegalConf
8 private static final String NoModel = "NO MODEL SELECTED";
12 ...
13 /** Constructor, which should be followed by selectModel */
14 public Model(Orders.Order order) {
15 super( order);
16 modelID = NoModel;
17 }
99 ...
100 /** Is the current binding of components to slots a legal
101 * configuration? Memo-ize the result for repeated calls */
102 public boolean isLegalConfiguration() {
103 if (! legalConfig) {
104 checkConfiguration();
105 }
106 return legalConfig;
107 }
108
109 /** Are all required slots filled with compatible components?
110 * It is impossible to assign an incompatible component,
111 * so just to check that every required slot is filled. */
112 private void checkConfiguration() {
113 legalConfig = true;
114 for (int i=0; i < slots.length; ++i) {
115 Slot slot = slots[i];
116 if (slot.required && ! slot.isBound()) {
117 legalConfig = false;
118 }
119 }
120 }
241 ...
242 }










Figure 15.1: Part of a Java implementation of class Model.

In object-oriented programs, public and private parts of a class (fields and methods) are distinguished. Private state and methods are inaccessible to external entities, which can only change or inspect private state by invoking public methods.[1] For example, the instance variable modelID of class Model in Figure 15.1 is accessible by external entities, but slots and legalConfig are accessible only within methods of the same class. The constructor Model() and the method checkConfiguration() can be used by external entities to create new objects and to check the validity of the current configuration, while method openDB() can be invoked only by methods of this class.


Encapsulated information creates new problems in designing oracles and test cases. Oracles must identify incorrect (hidden) state, and test cases must exercise objects in different (hidden) states.


Object-oriented programs include classes that are defined by extending or specializing other classes through inheritance. For example, class Model in Figure 15.1 extends class CompositeItem, as indicated in the class declaration. A child class can inherit variables and methods from its ancestors, overwrite others, and add yet others. For example, the class diagram of Figure 15.3 shows that class Model inherits the instance variables sku, units and parts, and methods validItem(), getUnitPrice() and getExtendedPrice(). It overwrites methods getHeightCm(), getWidthCm(), getDepthCm() and getWeightGm(). It adds the instance variables baseWeight, modelID, heightCm, widthCm, DepthCm, slots and legalConfig, and the methods selectModel(), deselect- Model(), addComponent(), removeComponent() and isLegalConfiguration().






Figure 15.3: An excerpt from the class diagram of the Chipmunk Web presence that shows the hierarchy rooted in class LineItem.








Inheritance brings in optimization issues. Child classes may share several methods with their ancestors. Sometimes an inherited method must be retested in the child class, despite not having been directly changed, because of interaction with other parts of the class that have changed. Many times, though, one can establish conclusively that the behavior of an inherited method is really unchanged and need not be retested. In other cases, it may be necessary to rerun tests designed for the inherited method, but not necessary to design new tests.


Most object-oriented languages allow variables to dynamically change their type, as long as they remain within a hierarchy rooted at the declared type of the variable. For example, variable subsidiary of method getYTDPurchased() in Figure 15.4 can be dynamically bound to different classes of the Account hierarchy, and thus the invocation of method subsidiary.getYTDPurchased() can be bound dynamically to different methods.












1 public abstract class Account {
151 ...
152 /**
153 * The YTD Purchased amount for an account is the YTD
154 * total of YTD purchases of all customers using this account
155 * plus the YTD purchases of all subsidiaries of this account;
156 * currency is currency of this account.
157 */
158 public int getYTDPurchased() {
159
160 if (ytdPurchasedValid) { return ytdPurchased; }
161
162 int totalPurchased = 0;
163 for (Enumeration e = subsidiaries.elements() ; e.hasMoreElements(); )
164 {
165 Account subsidiary = (Account) e.nextElement();
166 totalPurchased += subsidiary.getYTDPurchased();
167 }
168 for (Enumeration e = customers.elements(); e.hasMoreElements(); )
169 {
170 Customer aCust = (Customer) e.nextElement();
171 totalPurchased += aCust.getYearlyPurchase();
172 }
173 ytdPurchased = totalPurchased;
174 ytdPurchasedValid = true;
175 return totalPurchased;
176 }
332 ...
333 }










Figure 15.4: Part of a Java implementation of Class Account. The abstract class is specialized by the regional markets served by Chipmunk into USAccount, UKAccount, JPAccount, EUAccount and OtherAccount, which differ with regard to shipping methods, taxes, and currency. A corporate account may be associated with several individual customers, and large companies may have different subsidiaries with accounts in different markets. Method getYTDPurchased() sums the year-to-date purchases of all customers using the main account and the accounts of all subsidiaries.

Dynamic binding to different methods may affect the whole computation. Testing a call by considering only one possible binding may not be enough. Test designers need testing techniques that select subsets of possible bindings that cover a sufficient range of situations to reveal faults in possible combinations of bindings.


Some classes in an object-oriented program are intentionally left incomplete and cannot be directly instantiated. These abstract classes[2] must be extended through subclasses; only subclasses that fill in the missing details (e.g., method bodies) can be instantiated. For example, both classes LineItem of Figure 15.3 and Account of Figure 15.4 are abstract.


If abstract classes are part of a larger system, such as the Chipmunk Web presence, and if they are not part of the public interface to that system, then they can be tested by testing all their child classes: classes Model, Component, CompositeItem, and SimpleItem for class LineItem and classes USAccount, UKAccount, JPAccount, EUAccount and OtherAccount for class Account. However, we may need to test an abstract class either prior to implementing all child classes, for example if not all child classes will be implemented by the same engineers in the same time frame, or without knowing all its implementations, for example if the class is included in a library whose reuse cannot be fully foreseen at development time. In these cases, test designers need techniques for selecting a representative set of instances for testing the abstract class.


Exceptions were originally introduced in programming languages independently of object-oriented features, but they play a central role in modern object-oriented programming languages and in object-oriented design methods. Their prominent role in object-oriented programs, and the complexity of propagation and handling of exceptions during program execution, call for careful attention and specialized techniques in testing.


The absence of a main execution thread in object-oriented programs makes them well suited for concurrent and distributed implementations. Although many object- oriented programs are designed for and executed in sequential environments, the design of object-oriented applications for concurrent and distributed environments is becoming very frequent.


Object-oriented design and programming greatly impact analysis and testing. However, test designers should not make the mistake of ignoring traditional technology and methodologies. A specific design approach mainly affects detailed design and code, but there are many aspects of software development and quality assurance that are largely independent of the use of a specific design approach. In particular, aspects related to planning, requirements analysis, architectural design, deployment and maintenance can be addressed independently of the design approach. Figure 15.5 indicates the scope of the impact of object-oriented design on analysis and testing.






Figure 15.5: The impact of object-oriented design and coding on analysis and testing.





[1]Object-oriented languages differ with respect to the categories of accessibility they provide. For example, nothing in Java corresponds exactly to the "friend" functions in C++ that are permitted to access the private state of other objects. But while details vary, encapsulation of state is fundamental to the object- oriented programming paradigm, and all major object-oriented languages have a construct comparable to Java's private field declarations.





[2]Here we include the Java interface construct as a kind of abstract class.















No comments: