« 5. Basic Pattern #2 - Object Genie | Main | 3. Basic Patterns »
December 26, 2004
4. Basic Pattern #1 - Template
Group a common sequence of steps for testing a module into a class and have subclasses specify unique test case data without changing the main algorithm's structure.
Customer tests involve navigating to a particular part of the application and trying different scenarios. Even though each scenario is unique, there are still many commonalities between test cases. For instance, the way that you navigate to the module and the assertions that are made along the way can be grouped into one place - the Template class, which is a test-specific application of the Template Method design pattern.6
When the application inevitably changes, the majority of updates will occur in the Template class instead of within the test cases. This is because the Template class encapsulates navigation and assertion logic, which is more vulnerable to application changes than the data contained in test cases.
After each update to the Template class, the modifications will cascade automatically to all subclasses. This has the added benefit of making each test case (subclass) simpler since it is only required to specify the unique aspects of the test instead of the entire sequence of test steps and assertions. This reduces test code duplication and standardizes common navigations and assertions of related test cases.
If not using this design pattern, then every time the application changed or a new test case was added, the test writer would have to copy or re-record all the navigations and assertions that are part of each test. In that case, the automation effort would incur serious maintenance costs.
Since the Template pattern is adaptable to application changes, it can be used early in the development cycle without having to wait for application completion. Templates can be created and then continually updated as features are developed. This lends itself to agile software development practices such as Test Driven Development7 where tests are written early in the software lifecycle and are an integral part of the development effort.
Figure 1 presents two traditional tests which are then contrasted with Figures 2 and 3 which use the Template pattern. These tests check the functionality of adding and removing items from a shopping cart.
Figure 1: Non-template test cases.
import junit.framework.*;
public class ShoppingCartTest extends TestCase {
public ShoppingCartTest(String pName) { super(pName); }
public static Test suite() { return new TestSuite(ShoppingCartTest.class); }
public void testAddItem() { //1st test case
login();
searchForItem1();
addItem1ToCart();
searchForItem2();
addItem2ToCart();
logout();
}
public void testRemoveItem() { //2nd test case
login();
searchForItem1();
addItem1ToCart();
addItem1ToCart();
removeItem1FromCart();
searchForItem2();
addItem2ToCart();
removeItem2FromCart();
logout();
}
//test tool specific implementations of methods
//(e.g. login(), searchForItem1(), etc.)
}
The first test logs in, searches for item one, adds it to the cart, searches for item two, adds it to the cart, and logs out. The second test logs in, searches for item one, adds it twice to the cart, removes one quantity of item one from the cart, searches for item two, adds it to the cart, removes item two from the cart, and logs out. These two traditional tests have the following common elements: logging in, searching for item one, searching for item two, and logging out. These common steps can be moved into a new Template class.
Figure 2: Template class.
import junit.framework.*;
public abstract class ShoppingCartTemplate extends TestCase {
public ShoppingCartTemplate(String pName) { super(pName); }protected void setUp() {
login();
searchForItem1();
cartAction1(); // <------ test hook 1
searchForItem2();
cartAction2(); //<-------- test hook 2
logout();
}
//Subclasses are supposed to implement these test hooks anyway they like.
abstract protected void cartAction1();
abstract protected void cartAction2();//test tool specific implementations of methods
//(e.g. login(), searchForItem1(), etc.)
}
The Template defines the main sequence of steps as 1) login, 2) search for item one, 3) do something (to be defined by subclasses), 4) search for item two, 5) do something else (to be defined by subclasses), and 6) log out. Note that JUnit's setUp() method is used to define the sequence of steps, which enables this sequence of steps to occur for every test case subclass.
Each test case can now be moved to a separate subclass and implement the abstract methods of the Template.
Figure 3: Two test cases using the Template.
public class ShoppingCartTest_AddItem extends ShoppingCartTemplate {
public ShoppingCartTest_AddItem(String pName) { super(pName); }
protected void cartAction1() {
addItem1ToCart();
}
protected void cartAction2() {
addItem2ToCart();
}
}public class ShoppingCartTest_RemoveItem extends ShoppingCartTemplate {
public ShoppingCartTest_RemoveItem(String pName) { super(pName); }
protected void cartAction1() {
addItem1ToCart();
addItem1ToCart();
removeItem1FromCart();
}
protected void cartAction2() {
addItem2ToCart();
removeItem2FromCart();
}
}
The Add test case (ShoppingCartTest_AddItem) implements the two abstract methods by adding items one and two to the cart, respectively. The Remove test case (ShoppingCartTest_RemoveItem) implements the first abstract method by adding item one twice and then removing it. It implements the second abstract method by adding item two to the cart and then removing it.
Even though it appears that there is not a great reduction in code size in this example, code consolidation becomes more evident in real life scenarios which contain dozens of test steps and hundreds of test cases.
Adding more tests is relatively easy with the Template pattern since one only needs to implement the two abstract methods of the Template (i.e. cartAction1() and cartAction2()) rather than having to write all the setup and navigation code. This consolidation of redundant test steps into one place can drastically reduce maintenance costs as the application continues to evolve. For instance, if the application were to change so that customers now had to go to a specials screen before being able to add any items to the their cart, our Template can be updated to account for this and have the change cascade to every test without having to modify each test individually.
Figure 4: Modification of Template.
import junit.framework.*;
public abstract class ShoppingCartTemplate extends TestCase {
public ShoppingCartTemplate(String pName) { super(pName); }protected void setUp() {
login();
gotoSpecials(); // <------ new test step added
searchForItem1();
cartAction1(); // <------ test hook 1
searchForItem2();
cartAction2(); // <------ test hook 2
logout();
}
abstract protected void cartAction1();
abstract protected void cartAction2();
}
Posted by Misha Rybalov at December 26, 2004 09:04 PM