Search |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Testing Java in an Object-Oriented Way
Tue, 2006-03-28
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Do | Don't |
| 1. Focus on interfaces. | 2. Be worried about the implementation. |
| 3. Mention semantic contract using interfaces. | 4. Let subtypes break this semantic contract of their parent types. |
| 5. Service decoupling using interfaces. | 6. Be coupled with specific concrete implementation. |
| 7. Family extension using interfaces. | 8. Break the family relationship using concrete implementation. |
| 9. Establish a family rule by abstract classes. | 10. Impose your own rule through concrete classes. |
| 11. Let interfaces answer all "what" about the system. | 12. Forget to mention "how" are you answering all "what" from interfaces. |
Java classes eventually are subclasses of java.lang.Object, and all Java classes inherit methods from Object. Although half of these methods are final and cannot be overridden, a few methods are overridable, and a few often must be overridden:
clone
toString
equals
hashCode
Following are a few unique features of objects in an object-oriented language like Java:
Object-oriented systems are therefore built on the solid foundation of classes, components, systems, subsystems, modules, and frameworks. Figure 1 briefly depicts an object-oriented system:
|
|
Where
Every object-oriented systems follow these eight most important characteristics throughout its lifecycle. Therefore they are considered SPECIAL.
From the above discussion it is clear that object-oriented testing should be performed on the basis of those eight SPECIAL features as defined in Figure 1. In OO testing, we are required to perform testing of interfaces, abstract classes, classes, frameworks, and systems. Figures 2 and 3 depicts the recommended type of testing that needs to be performed for each of these OO constructions apart from a regular behavioral test:

Figure 2. Object-oriented testing methodologies

Figure 3. Relationship of OO testing methodologies
SPECIAL is an acronym that you should remember in every piece of object-oriented software to put under test. In the object-oriented sense, the contract needs to be fully testable through one reusable test suite that tests functional compliance (i.e., a module's compliance with some published functional specification, enforced through interfaces or abstract classes) with the interface that could be applied to each of the implementing classes.
The next few sections will mainly concentrate on pattern-based object-oriented testing strategies and different effective object-oriented testing techniques.
Strategy is a bind-once design pattern that defines a family of algorithms, encapsulates each one, and makes them interchangeable. Figure 4 shows the Strategy pattern UML diagram:

Figure 4. Strategy pattern
Here is a very simple example of an InterestCalculator interface with a few simple methods to calculate the actual interest value. We will construct an abstract strategy for this interface, and then look at the reusability options of the same interface implementations.
public interface InterestCalculator
{
public void acceptAmount (double dAmt);
public double getInterestRate (float fInterest);
public double getInterest();
}
An abstract test is a test class, declared abstract and based
on the Strategy pattern design shown in Figure 4. This abstract test class
is the heart of the Strategy design patterns and it has following characteristics:
StrategicContext interface.public interface IStrategicContext
{
public void testMethod1();
public void testMethod2();
}
The form of a JUnit abstract test is something like this:
import junit.framework.*;
public abstract class AbstractStrategy
extends TestCase implements IStrategicContext
{
private CandidateInterface candidateInterface;
public AbstractTestSample(String name)
{
super(name);
}
// Not intended to be overridden by
// ConcreteStrategy classes
public final void setUp() throws Exception
{
//Create the object to be tested.
candidateInterface =
createCandidateInterface();
//assert that result is non-null
assertNotNull(candidateInterface);
}
// This abstract method is acting as a
// factory to create the candidate
// interface
public abstract CandidateInterface createCandidateInterface()
throws Exception;
// testMethod1 is final and
// can't be overwritten by
// ConcreteStrategy classes
public final void testMethod1()
{
// Method1 body
}
// testMethod2 is final and
// can't be overwritten by
// ConcreteStrategy classes
public final void testMethod2()
{
//Method2 body
}
//Declare n number of
// methods to be tested
// [...]
}
Based on the above recommendations, we can easily create an abstract strategy class to test InterestCalculator.
Now we can think of different implementations of the InterestCalculator interface, like MonthlyInterestCalculator, YearlyInterestCalculator, etc. For each of these implementations, we'd need to construct a separate ConcreteStrategyClass extending AbstractStrategy. Therefore, the AbstractStrategy class is mostly reusable across the same hierarchy testing. However, each ConcreteStrategyClass will be responsible for deriving its own set of strategies required for particular testing and will generally vary widely depending on the requirements.
This abstract-strategy-based test design techniques has several benefits, which are listed below:
In this section we will discuss a few important object-oriented feature testing techniques in Java.
In Java, the State design pattern is the best way to perform state-based testing arrangements. The diagram in Figure 5 represents the State design pattern, or the design strategy of a state machine:

Figure 5. State design pattern
State needs to be abstracted around a state class. Each implementation is built as a separate ConcreteState class. The Strategy pattern described in the previous section is the best way to test Java-based object state. The Java API provides a class called java.util.Observable, which can be extended by any Java class to provide state-change notification.
Polymorphism is the property of a Java class to be in a different forms. Polymorphism may be single or multiple, and may be of a parametric type or ad-hoc (operator-overloading type). Somewhat differently, Luca Cardelli and Peter Wegner categorize polymorphism under two major categories: ad hoc and universal (see "On Understanding Types, Data Abstraction, and Polymorphism" [PDF]). These two categories may be further classified as:
Among all these four different types of polymorphism, parametric is the best way to achieve polymorphism. Inclusion is also good. Java compilers extensively use the coercion and overloading types of polymorphism internally (e.g., implicit type conversion, operator overloading, etc.). However, developers mostly use the parametric and inclusion types of polymorphism to generate different subtypes and use them in a type-safe way.
In Java, polymorphism is manifested in three ways: method overloading, method overriding through inheritance, and method overriding through the Java interface. Polymorphism testing is required to ensure type safety.
In fact, Java polymorphism could me understood as a particular state of an object at a particular moment. Therefore, to test polymorphism of any Java class, we need to create a one-to-one concrete strategy to test the class for each concrete state.
Encapsulation refers to information hiding achieved via access modifiers in Java. It is sometimes necessary to test a private variable in Java, especially when it is related to state testing. However, in JUnit, there is no direct support of performing private method testing. But using JUnit add-ons such as junitx.util.PrivateAccessor, we can access private methods and test the values with regular JUnit tests.
Interfaces and abstract classes are the only way to specify the contract between different Java objects. Interfaces are the heart of real object-oriented designs and therefore must be tested properly using the Abstract Strategy pattern discussed earlier.
Inheritance can be used to implement specialization relationships. There are different types of inheritances possible, which is beyond the scope of this article. However, it is always recommended that you use the specialization and extension types of inheritance, as the resultant classes better support the aforementioned five object-oriented principles.
On the other hand, specification, construction, and limitation type inheritance are not recommended, as they restrict class reusability. Inheritance testing is really important for large organizations where objects are built in geographically distributed enterprise environments and may lose traceability. Figure 6 shows one of the recommended ways to derive inheritance hierarchy.

Figure 6. Class hierarchy and family extension
A factory method that actually helps the polymorphism is the one of the best ways to use the inheritance.
Public abstract class CreditCard
{
public abstract void createCard();
//...
}
Public class MasterCard extends CreditCard
{
public void createCard()
{
// do implementation
}
//...
}
Public class VisaCard extends CreditCard
{
public void createCard()
{
// do implementation
}
//...
}
Public class CreateCardPrintDept
{
public void getCard(CreditCard creditCard )
throw Exception
{
createCard(new VisaCard());
// Check null objects etc.
//...
createCard(new MasterCard());
// Check null objects etc.
//..
}
public static void printCard
(CreditCard creditCard)
{
creditCard.createCard();
}
}
This example also points out that using factory type object-creation techniques results in type-safe object creation. In other words, these subtypes satisfy the "principle of substitutability," allowing objects to be used interchangeably, which is an important objective of using inheritance. Inheritance is easily testable through the previously discussed AbstractStrategy classes and using instanceof methods like:
if (obj instanceof class1)
...
else if (obj instanceof class2)
...
else if (obj instanceof class3)
Association could be of three different types:
createAccount (SavingsAccount))private Account account)private Account savings; Account = new Account())For association testing, follow these tips:
You may use the JUnit assertXXX method to compare these values as regular JUnit tests.
This is the same as interface testing.
Many large-scale object-oriented applications are developed and maintained by many people working across locations. Most of these object-oriented systems, APIs, frameworks, etc. are intended to be reusable. However, erroneous implementations of these methods can cause a semantic breaking of contracts and make it impossible for others to subtype them.
Therefore, another very important type of testing is to ensure that broken contracts are well-tested. This type of testing ensures that in case of a broken contract, system behaviors are correct and graceful decision logic is maintained. During loss-of-contract scenarios, the system should raise appropriate exceptions. State machines are very useful to test these exceptional situations; mock objects are another candidate to test exceptions.
So far we have discussed different kinds of object-oriented testing techniques, which are very important from any object-oriented system's point of view. However, another important part of these testing strategies is to understand and identify an appropriate lifecycle phase for each of these types of testing. The primary difference between object-oriented testing and normal procedural testing is that object-oriented testing is better performed by state and strategy patterns, which need to be very carefully designed by the architects. Otherwise, it will be trapped under the common symptoms of testing objects in a procedural way. There are different processes and methodologies available on the market. However, EUP is the only process that extends enterprise and RUP is the development oriented process. The following table provides the phases where OO testing might add significant value:
| Phase | Process or methodology | What do we test? |
| Enterprise architecture | EUP | Framework |
| Analysis and design | RUP | Interfaces, abstract classes and system |
| Implementation | RUP | Concrete implementation |
| Test | RUP | Actual testing |
The following table briefly summarizes OO testing concepts:
| What (feature of the OO concept) | Why (do need to test it) | How (do you achieve that) | Which (stage of the process) | Who (typically responsible for) |
| State | Determine the correct state | State pattern | Development | Architect, developer |
| Polymorphism | To confirm the type safety | State pattern | Architecture design, development | Architect, developer |
| Encapsulation | Correct data | JUnit Add-on | Development | Developer |
| Contract/Interfaces | Correct contract/interface specification | Strategy pattern | Architecture design, development | Architect, developer |
| Inheritance | Proper hierarchy/subtype | Strategy pattern | Architecture design, development | Architect, developer |
| Association | Correct relationship | State and Strategy patterns | Development | Developer |
| Abstraction | Correct behavior | Strategy | Architecture design, development | Architect, developer |
| Loss of contract | Correct behavior even in the case of exception or lost-a-contract scenario | State, mock object | Development | Architect, developer |
| Note: All of these state- and strategy-pattern-based testing frameworks should be designed by the architects, with the help of the developers. However, in the actual development stage, developers will be responsible to ensure correct usage of them. | ||||
There are different schools of thought in the market. Moreover, in this consulting world, people argue against the usage of OO testing and brand it as a useless waste of time. In reality, it's really very important to perform object-oriented testing, if your software is written with the intention of reusability from either system or framework points of view. It is very important to test open source and third-party software and, OO testing is the only way to test semantic contracts. In any organization, creating an object-oriented testing framework can reduce the cost of testing an application by a significant order of magnitude, because it lets you reuse both design and code.
Standards
Bibliographies
Research Papers and Publications
Internet Materials
|
|