JaVa
   

Case Study: Adding an Entity Bean to the Pet Store

This section explains the test cases for the baseline version of the pet store. We discuss adding an entity bean and Web form to manage product data over the Web (that is, to add, delete, and modify product data). Once we add the entity bean, we add more test cases to test it, and then we integrate those tests into the test buildfile. Thus we will cover building and deploying an entity bean to our EJB server. Before we talk about including EJBs, let's look at what we already have for testing the system. Remember, before you refactor, you should have tests set up so you know that what you refactor does not break.

Overview of Existing JUnit Tests

The test buildfile runs four JUnit tests, as follows:

Each test class tests its corresponding counterpart in the model—that is, the public interface to the system. Complete code listings for these tests and their interfaces can be found in Appendix A. Because the structure of these four tests are very similar, this section describes only two of them in detail for purposes of illustrating unique elements and techniques. These tests are run by the test project buildfile, demonstrated with this test buildfile snippet (the complete code will be discussed later):

 <target depends="compile">
. . .
 <test name="test.xptoolkit.model.CategorySystemTest" todir="${reports}" />
 <test name="test.xptoolkit.model.CategoryTest" todir="${reports}" />
 <test name="test.xptoolkit.model.SubcategoryTest" todir="${reports}" />
 <test name="test.xptoolkit.model.ProductTest" todir="${reports}" />
. . .


The tests are designed to test all the functionality of the public interface—everything that could possibly break. The following listing for CategorySystemTest tests the CategorySystem.

package test.xptoolkit.model;
import xptoolkit.petstore.model.CategorySystem;
import junit.framework.*;
public class CategorySystemTest extends TestCase {
 CategorySystem system;
 public static void main(java.lang.String[] args) {
 junit.textui.TestRunner.run(suite());
 }
 public static Test suite() {
 TestSuite suite = new TestSuite(CategorySystemTest.class);
 return suite;
 }
 protected void setUp()throws Exception {
 system = new CategorySystem();
 }
 /** Test of getCurrentCategory method, of class xptoolkit.petstore.model.CategorySystem. */
 public void testGetCurrentCategory() throws Exception{
 assertNotNull(system.getCurrentCategory());
 }
 
 /** Test of getSubcategory method, of class xptoolkit.petstore.model.CategorySystem. */
 public void testGetSubcategory() throws Exception{
 assertNotNull(system.getSubcategory(111));
 }
 /** Test of getProduct method, of class xptoolkit.petstore.model.CategorySystem. */
 public void testGetProduct() throws Exception {
 testGetSubcategory();
 assertNotNull(system.getProduct(1));
 }
 /** Test of getCurrentSubcategory method, of class xptoolkit.petstore.model.CategorySystem. */
 public void testGetCurrentSubcategory() throws Exception{
 testGetSubcategory();
 assertNotNull(system.getCurrentSubcategory());
 }
 /** Test of getCurrentProduct method, of class xptoolkit.petstore.model.CategorySystem. */
 public void testGetCurrentProduct() throws Exception{
 testGetSubcategory();
 testGetProduct(); assertNotNull(system.getCurrentProduct());
 }
}


As you may remember from , we populate the database with default values. The JUnit tests use that information to navigate the category hierarchy (for example, the following listing shows the CategoryTest).

package test.xptoolkit.model;
import java.util.*;
import junit.framework.*;
import xptoolkit.petstore.model.Category;
import xptoolkit.petstore.model.Product;
import xptoolkit.petstore.model.Subcategory;
public class CategoryTest extends TestCase {
 Category category; //object under test
 public static void main(java.lang.String[] args) {
 junit.textui.TestRunner.run(suite());
 }
 public static Test suite() {
 TestSuite suite = new TestSuite(CategoryTest.class);
 return suite;
 }
 public void setUp()throws Exception{
 category = Category.getCategory();
 category.setId(777);
 }
 /** Test of getCategory method, of class xptoolkit.petstore.model.Category. */
 public void testGetCategory() throws Exception{
 System.out.println("testGetCategory");
 Category category = Category.getCategory();
 category.setId(777);
 this.assertNotNull("category", category);
 }
 /** Test of getSubcategories method, of class xptoolkit.petstore.model.Category. */
 public void testGetSubcategories() throws Exception {
 Subcategory [] categories = category.getSubcategories();
 assertNotNull("categories", categories);
 for (int index=0; index < categories.length; index++){
 assertNotNull("subcategory", categories[index]);
 }
 }
 /** Test of getSubcategory method, of class xptoolkit.petstore.model.Category. */
 public void testGetSubcategory() throws Exception {
 Subcategory [] categories = category.getSubcategories();
 assertNotNull("categories", categories);
 for (int index=0; index < categories.length; index++){
 Subcategory subcat=categories[index];
 int id = subcat.getId();
 assertNotNull("subcategory", category.getSubcategory(id));
 }
 }
 public void testGetters() throws Exception {
 assertNotNull("name", category.getName());
 assertNotNull("description", category.getDescription());
 }
}


There are four tests to test the public interface of xptoolkit.petstore.model.Category, as follows:

The setUp() method creates a category instance and sets it to the main test category.

 category = Category.getCategory();
 category.setId(777);


Remember, setUp() is called for each test. Thus, each test gets its own copy of category. For example, testGetSubcategories gets its own copy of category, which it uses to test the getSubcategories() method of xptoolkit.petstore.model.Category as follows:

 public void testGetSubcategories() throws Exception {
 Subcategory [] categories = category.getSubcategories();
 assertNotNull("categories", categories);
 for (int index=0; index < categories.length; index++){
 assertNotNull("subcategory", categories[index]);
 }
 }


Because the test data is known, we could check for specific values of subcategories in specific locations. For example, look at testGetters() from the ProductTest class:

/** Test of getters method of class
 xptoolkit.petstore.model.Product. */
 public void testGetters() {
 this.assertEquals("name", product.getName(), "Poodle");
 this.assertEquals("description", product.getDescription(), "Poodle description");
 testSetters();
 this.assertEquals("name", product.getName(), "Boo");
 this.assertEquals("description", product.getDescription(), "Designer");
 }


Note that each main model class in the model project has a corresponding test class in the test project:

Java Click To expand

The test buildfile is responsible for executing the tests in an automated fashion (see the following listing). Typically, you write the tests as you develop your classes. Once you are done writing your tests and using them to incrementally test your code as you write it, you put them in your test buildfile so they are included in your automatic build and deploy. Then, not only can you break the build by checking in code that does not compile, you can break the build by writing code that breaks the tests.

<project default="all" >
 <target unless="setProps" description="setup the properties.">
 <property value="/tmp/petstore" /> </target>
 <target depends="setProps" description="initialize the properties.">
 <tstamp/>
 <property name="local_outdir" value="${outdir}/pettest" />
 <property value="${outdir}/lib" />
 <property value="${outdir}/dist" />
 <property value="${outdir}/reports" />
 <property value="${local_outdir}/testclasses" />
 <property name="build_lib" value="./../lib" />
 <property name="test_lib" value="./../testlib" />
 <property name="test_jar" value="${lib}/pettest.jar" />
 </target>
 <target depends="init" description="clean up the output directories.">
 <delete dir="${local_outdir}" />
 <delete dir="${reports}" />
 </target>
 <target depends="init" description="prepare the output directory.">
 <mkdir dir="${build}" />
 <mkdir dir="${dist}" />
 <mkdir dir="${reports}" />
 <mkdir dir="${reports}/html" />
 </target>
 <target depends="prepare" description="compile the Java source.">
 <javac srcdir="./java" destdir="${build}">
 <classpath >
 <fileset dir="${lib}">
 <include name="**/*.jar"/>
 </fileset>
 <fileset dir="${build_lib}">
 <include name="**/*.jar"/>
 </fileset>
 <fileset dir="${test_lib}">
 <include name="**/*.jar"/>
 </fileset>
 </classpath>
 </javac>
 </target>
 <target depends="compile">
 <jar jarfile="${test_jar}"
 basedir="${build}" />
 </target>
 <target depends="compile">
 <junit printsummary="true" fork="yes">
 <formatter type="xml" />
 <test name="test.xptoolkit.model.CategorySystemTest" todir="${reports}" />
 <test name="test.xptoolkit.model.CategoryTest" todir="${reports}" />
 <test name="test.xptoolkit.model.SubcategoryTest" todir="${reports}" />
 <test name="test.xptoolkit.model.ProductTest" todir="${reports}" />
 <classpath> <fileset dir="${lib}">
 <include name="**/*.jar"/>
 </fileset>
 <fileset dir="${build_lib}">
 <include name="**/*.jar"/>
 </fileset>
 <fileset dir="${test_lib}">
 <include name="**/*.jar"/>
 </fileset>
 <fileset dir="/tools/ant/lib">
 <include name="**/*.jar"/>
 </fileset>

 <fileset dir="${build}" />
 </classpath>
 </junit>
 <junitreport todir="${reports}">
 <fileset dir="${reports}">
 <include name="TEST-*.xml"/>
 </fileset>
 <report format="frames" todir="${reports}/html"/>
 </junitreport>
 </target>
 <target depends="clean,package,test" />
 <target depends="package,test" description="perform all targets."/> </project>


The focal point of the test buildfile is the test target. The test target runs each of the tests that we defined, such as test.xptoolkit.model.CategoryTest. The test target uses the junit and junitreport tasks as follows:

 <target depends="compile">
 <junit printsummary="true" fork="yes">
 <formatter type="xml" />
 <test name="test.xptoolkit.model.CategorySystemTest" todir="${reports}" />
 . . .
 <classpath> . . .
 <fileset dir="${test_lib}">
 <include name="**/*.jar"/>
 </fileset>
 . . .
 </classpath>
 </junit>
 <junitreport todir="${reports}">
 <fileset dir="${reports}">
 <include name="TEST-*.xml"/>
 </fileset>
 <report format="frames" todir="${reports}/html"/>
 </junitreport>
 </target>


Notice that the classpath sub-element of the JUnit class uses the JAR files in ${test_lib}. This is where we store the JAR file (junit.jar) that contains the JUnit framework classes. The junitreport task specifies the output directory as "${reports}/html" and the input test files that are the XML files generated from the output of the junit task. The junitreport task also specifies the output directory in which to put the report HTML files. (You can see examples of the output HTML displayed in a browser in .) The following figure shows the output of this test.

Java ScreenShot

Adding an Enterprise JavaBean to the Pet Store

This section adds a feature to the baseline pet store app: the ability to add, edit, and delete products from the Web. We have decided to use a container-managed entity EJB. Thus, we need to do the following:

  1. Create the product entity bean.

  2. Add a new subproject buildfile for EJBs.
  3. Add a new test case class to test our Product EJB.
  4. Update our categorySystem class.
  5. Create an HTML form.
  6. Add additional JSPs to handle the form submission and the backend navigation.

The next figure shows a block diagram of how the output will look when we are done.

Java Click To expand
Java Start Sidebar
Security and Authentication

In the real world, you should password-protect your product entry management. Servlets 2.3 enables you to do this with a servlet filter. Or, you can do this with Web authentication provided by your app server or Web server. This example does not demonstrate authentication, but you can add it without changing the JSPs introduced with any of the methods we use.

Java End Sidebar

The entity bean is fairly simple, because it is uses container managed persistence (CMP). The container takes care of persisting the bean to the database. See the next three listings for the complete product entity bean: its interface, implementation, and home, respectively. With this product entity bean we add, delete, and edit product entries. You can use a number of techniques and design patterns to reduce the number of remote procedure calls; for simplicity, we don't use them here.

package xptoolkit.petstore.entity;
import javax.ejb.*;
import java.rmi.RemoteException;
public interface Product extends EJBObject {
 public String getDescription()throws RemoteException;
 public String getName()throws RemoteException;
 public Integer getId()throws RemoteException;
 public int getSubcategoryId()throws RemoteException;
 public int getQty()throws RemoteException;
 public java.math.BigDecimal getPrice()throws RemoteException;
 public void setDescription(String description)throws RemoteException;
 public void setName(String name)throws RemoteException;
 public void setId(Integer ID)throws RemoteException;
 public void setSubcategoryId(int subcat)throws RemoteException;
 public void setQty(int qty)throws RemoteException;
 public void setPrice(java.math.BigDecimal price)throws RemoteException;
}
package xptoolkit.petstore.entity;
import javax.ejb.*;
public class ProductBean implements EntityBean {
 EntityContext ctx;
 public String description;
 public String name; public Integer id;
 public int subcategoryId; public int qty;
 public java.math.BigDecimal price;
 public Integer ejbCreate (Integer id, String name, int qty, String description, int subCat, java.math.BigDecimal price){
 this.id = id;
 this.name=name;
 this.description=description;
 this.qty=qty=0;
 subcategoryId=subCat; this.price= price;
 return null;
 }
 public void ejbPostCreate(Integer _id, String name, int qty, String description, int subCat, java.math.BigDecimal price){
 id = (Integer)ctx.getPrimaryKey();
 System.out.println("Product ID " + id);
 }
 public String getDescription(){return description;}
 public String getName(){return name;}
 public Integer getId(){return id;}
 public int getSubcategoryId(){return subcategoryId;}
 public int getQty(){return qty;}
 public java.math.BigDecimal getPrice(){return price;}
 public void setDescription(String description)
 {this.description =description;}
 public void setName(String name){this.name=name;}
 public void setId(Integer id){this.id=id;}
 public void setSubcategoryId(int subcategoryId)
 {this.subcategoryId=subcategoryId;}
 public void setQty(int qty){this.qty=qty;}
 public void setPrice(java.math.BigDecimal price){this.price=price;}
 public void setEntityContext(EntityContext ctx) { this.ctx = ctx; }
 public void unsetEntityContext() { ctx = null; }
 public void ejbActivate() { }
 public void ejbPassivate() { }
 public void ejbLoad() { }
 public void ejbStore() { }
 public void ejbRemove() { }
}
package xptoolkit.petstore.entity;
import javax.ejb.*;
import java.util.*;
import java.rmi.RemoteException;
public interface ProductHome extends EJBHome {
 public Product create(Integer id, String name, int qty, String description, int subCat, java.math.BigDecimal price) throws RemoteException, CreateException;
 Product findByPrimaryKey(Integer key) throws RemoteException, FinderException;
 Collection findAll()throws RemoteException, FinderException;
 Collection findBySubcategoryId()throws RemoteException, FinderException;
}
<ejb-jar>
<description>
This ejb-jar files contains the Enterprise beans for the Petstore Case Study
</description>
<enterprise-beans>
 <entity>
 <ejb-name>ProductBean</ejb-name>
 <home>xptoolkit.petstore.entity.ProductHome</home>
 <remote>xptoolkit.petstore.entity.Product</remote>
 <ejb-class>xptoolkit.petstore.entity.ProductBean</ejb-class>
 <persistence-type>Container</persistence-type>
 <prim-key-class>java.lang.Integer</prim-key-class>
 <primkey-field>id</primkey-field>
 <reentrant>False</reentrant>
 <cmp-field><field-name>description</field-name></cmp-field>
 <cmp-field><field-name>name</field-name></cmp-field>
 <cmp-field><field-name>id</field-name></cmp-field>
 <cmp-field><field-name>subcategoryId</field-name></cmp-field>
 <cmp-field><field-name>qty</field-name></cmp-field>
 <cmp-field><field-name>price</field-name></cmp-field>
 </entity>
</enterprise-beans>
<assembly-descriptor>
 <container-transaction>
 <method>
 <ejb-name>ProductBean</ejb-name>
 <method-name>*</method-name>
 </method>
 <trans-attribute>Required</trans-attribute>
 </container-transaction>
</assembly-descriptor>
</ejb-jar>


The new ProductTest (test.xptoolkit.petstore.entity.ProductTest) is used to test the entity bean (xptoolkit.petstore.entity.Product). The ProductTest simulates a client; thus, it must import the needed client-side EJB classes. It must import the Java Naming and Directory Interface (JNDI) support and the RMI PortableRemoteObject as follows:

import javax.rmi.PortableRemoteObject; import javax.naming.*;


Of course, it must import the bean's home and remote interface, as follows:

import xptoolkit.petstore.entity.Product;
import xptoolkit.petstore.entity.ProductHome;


Because every test needs to access the home interface to create, find, and delete Products, we locate an instance of the home interface using JNDI in the setUp() method as follows:

protected void setUp()throws Exception{
 Object ref;
 InitialContext jndiContext=null;
 jndiContext = new InitialContext(env);
 ref = jndiContext.lookup("ProductBean");
 home = (ProductHome) PortableRemoteObject.narrow (ref, ProductHome.class);
}


This is fairly standard client-side EJB code. The first test uses the home interface to create a product entity with test data.

 public void testCreate()throws Exception{
 product = home.create(new Integer(876662), "Rick", 5, "you ", 555,
 new java.math.BigDecimal(1200));
 assertNotNull("product", product);
 assertEquals("name", "Rick", product.getName());
 } 


As you can see, the first and second tests depend on each other; they are order dependent. The scenario works because the test framework uses reflection, and reflection uses the methods in the order they are declared. This code is brittle and depends on some minutia in the Java reflection API. You can ensure the order of execution by explicitly setting it in the suite() method instead of relying on the generic reflection-based methods. The TestSuite has an addTest() method that lets you add test cases to it. You could also use OrderedTestSuite, which is available in junit-addons (http://sf.net/projects/junit-addons). The second test finds the entity created with the first test and then deletes that entity by calling the product entities' remove() method. Then, to make sure the entity was removed, the test tries to find it again. If the home interface's findByPrimaryKey() method does not find the object, we return; otherwise, we force a fail by calling fail(), as follows:

 public void testRemove()throws Exception{
 product = home.findByPrimaryKey(new Integer(876662));
 product.remove();
 try{
 product = home.findByPrimaryKey(new Integer(876662));
 }
 catch(javax.ejb.ObjectNotFoundException e){
 return;
 }
 fail("Product entity should already be gone and not findable.");
 }


The other methods test the setter and getter methods of the product, as follows:

 /** Test of setter methods, of class xptoolkit.petstore.model.Product. */
 public void testSetters() throws Exception{
 testCreate();
 product.setName("Boo");
 product.setDescription("Designer");
 product.setQty(5);
 testRemove();
 }
 /** Test of getter methods, of class xptoolkit.petstore.model.Product. */
 public void testGetters() throws Exception{
 testCreate(); this.assertEquals("name", product.getName(), "Rick");
 this.assertEquals("description", product.getDescription(), "you ");
 product.setName("Boo");
 product.setDescription("Designer");
 product.setQty(5);
 this.assertEquals("name", product.getName(), "Boo");
 this.assertEquals("description", product.getDescription(), "Designer");
 testRemove();
 }


The tests in ProductTest are quite simple. They ensure that we have set up and created our entity bean correctly. See the following list for the complete ProductTest code.

package test.xptoolkit.petstore.entity;
import junit.framework.*;
import java.util.Properties;
import javax.rmi.PortableRemoteObject; import javax.naming.*;
import xptoolkit.petstore.entity.Product;
import xptoolkit.petstore.entity.ProductHome;
public class ProductTest extends TestCase {
 Product product;
 ProductHome home;
 protected void setUp()throws Exception{
 Object ref;
 InitialContext jndiContext=null;
 jndiContext = new InitialContext(env);
 ref = jndiContext.lookup("ProductBean");
 home = (ProductHome) PortableRemoteObject.narrow (ref, ProductHome.class);
 }
 public static void main(java.lang.String[] args) {
 junit.textui.TestRunner.run(suite());
 }
 public static Test suite() {
 TestSuite suite = new TestSuite(ProductTest.class);
 return suite;
 }
 public void testCreate()throws Exception{
 product = home.create(new Integer(876662), "Rick", 5, "you ", 555,
 new java.math.BigDecimal(1200));
 assertNotNull("product", product);
 assertEquals("name", "Rick", product.getName());
 }
 public void testRemove()throws Exception{
 product = home.findByPrimaryKey(new Integer(876662));
 product.remove();
 try{
 product = home.findByPrimaryKey(new Integer(876662));
 }
 catch(javax.ejb.ObjectNotFoundException e){
 return;
 }
 fail("Product entity should already be gone and not findable.");
 }
 /** Test of getSetter methods, of class xptoolkit.petstore.model.Product. */
 public void testSetters() throws Exception{
 testCreate();
 product.setName("Boo");
 product.setDescription("Designer");
 product.setQty(5);
 testRemove();
 }
 /** Test of getter methods, of class xptoolkit.petstore.model.Product. */
 public void testGetters() throws Exception{
 testCreate(); this.assertEquals("name", product.getName(), "Rick");
 this.assertEquals("description", product.getDescription(), "you ");
 product.setName("Boo");
 product.setDescription("Designer");
 product.setQty(5);
 this.assertEquals("name", product.getName(), "Boo");
 this.assertEquals("description", product.getDescription(), "Designer");
 testRemove();
 }
 static Properties env = new Properties();
 static { env.setProperty("java.naming.factory.initial", "org.jnp.interfaces.NamingContextFactory");
 env.setProperty("java.naming.provider.url", "localhost:1099");
 env.setProperty("java.naming.factory.url.pkgs", "org.jboss.naming");
 }
}


Java Start Sidebar
IDEs Support JUnit and Ant

Plug-ins are available for Eclipse, Forte, NetBeans, TogetherSoft ControlCenter, JBuilder, and so on for both JUnit and Ant. We create many of our tests by generating the tests' started code in the NetBeans IDE. The ProductTest started code was initially generated with NetBeans support for JUnit. You specify the class, and NetBeans generates the started skeleton to test your class. Cool beans!

Java End Sidebar

It's our considered opinion that no JSP should know whether you are using JDBC, flat files, or entity beans to manage the persistence of the system. Thus, we decided to add support for adding and removing products behind the CategorySystem façade class. In fact, the implementation of the client-side piece of the Product entity is in CategoryDB, and the public interface is defined in the Category abstract class. Here are the additional methods that we added to the CategorySystem class (xptoolkit.petstore.model .CategorySystem):

 public void createProduct(Product product) throws Exception{
 currentCategory.createProduct(product);
 if(currentSubcategory!=null)currentSubcategory.invalidate();
 }
 public void editProduct(Product product) throws Exception{
 currentCategory.editProduct(product);
 if(currentSubcategory!=null)currentSubcategory.invalidate();
 }
 public void deleteProduct(int id) throws Exception{
 currentCategory.deleteProduct(id);
 if(currentSubcategory!=null)currentSubcategory.invalidate();
 }


Here are the corresponding methods we added to the Category class:

 public abstract void createProduct(Product product) throws Exception;
 public abstract void editProduct(Product product) throws Exception;
 public abstract void deleteProduct(int id) throws Exception; 


The actual implementation of these methods is in the CategoryDB class, as follows:

 public void createProduct(Product product) throws Exception{
 getHome().create(new Integer(product.getId()), product.getName(), product.getQty(), product.getDescription(), product.getFkSubcategoryId(),
 new java.math.BigDecimal(product.getPrice()));
 }
 public void editProduct(Product product) throws Exception{
 xptoolkit.petstore.entity.Product p
 =getHome().findByPrimaryKey(
new Integer(product.getId()));
 p.setName(product.getName());
 p.setDescription(product.getDescription());
 p.setPrice(new java.math.BigDecimal(product.getPrice()));
 p.setQty(product.getQty());
 }
 public void deleteProduct(int id) throws Exception{
 getHome().findByPrimaryKey(new Integer(id)).remove();
 }
 private ProductHome getHome() throws Exception{
 Object ref;
 InitialContext jndiContext=null;
 jndiContext = new InitialContext(env);
 ref = jndiContext.lookup("ProductBean");
 return (ProductHome) PortableRemoteObject.narrow (ref, ProductHome.class);
 } 


This code should look familiar. It is much like the code in our test, except that now we are using it to implement the public interface to our Web app. You should add tests at the boundary points to every tier in an n-tier architecture. That way, you test the public interface of each tier. This approach becomes particularly useful if things begin to go wrong; when debugging a distributed multitiered app, it's helpful to be able to test access to each tier independently from the rest of the added business logic in the encapsulating tier. Because the CategorySystem is a very thin wrapper in the case of adding, removing, and editing products, we decided to add the tests in the CategoryTest as follows:

 public void testCreateDeleteProduct() throws Exception {
 . . .
 Product p = new Product(){};
 p.setId(1119996);
 p.setFkSubcategoryId(111);
 p.setName("Test1");
 p.setDescription("Test1 Description");
 p.setPrice(11);
 p.setPrice(6); category.createProduct(p);
 Product p2 = category.getProduct(1119996);
 assertEquals("name after create",p2.getName(), p.getName());
 p.setName("Test2");
 category.editProduct(p);
 Product p3 = category.getProduct(1119996);
 assertEquals("name after edit", p3.getName(), p.getName());
 category.deleteProduct(p.getId());
 Product p4 = category.getProduct(1119996);
 this.assertEquals("product should be gone", -1, p4.getId());
 }


This code is fairly simple, because the actual product implementation is tested thoroughly in the entity ProductTest. Essentially, the test creates a product, edits it, and deletes it. It makes sure the product data is added, edited, and removed. Here the test creates the product by calling the category createProduct() method:

 Product p = new Product(){};
 p.setId(1119996);
 p.setFkSubcategoryId(111);
 p.setName("Test1");
 p.setDescription("Test1 Description");
 p.setPrice(11);
 p.setPrice(6); category.createProduct(p);


Next, the test makes sure that the product actually was created by looking it up:

 Product p2 = category.getProduct(1119996);
 assertEquals("name after create",p2.getName(), p.getName());


Here the test edits the product by changing the product object and then submitting it. Then, the test makes sure the product was edited:

 p.setName("Test2");
 category.editProduct(p);
 Product p3 = category.getProduct(1119996);
 assertEquals("name after edit", p3.getName(), p.getName());


Finally, the test removes the product, as follows:

 category.deleteProduct(p.getId());
 Product p4 = category.getProduct(1119996);
 this.assertEquals("proudct should be gone", -1, p4.getId());


One thing is wrong with this test. It should be further functionally decomposed. For example, let's say the create part fails or the delete part fails. The output of the test will not make clear which functionality was not working. There is a fine line between being over-cautious and sloppy. The more you functionally decompose, the better your reports will be able to point you to the correct failure point. So, let's decompose the test a little further. We can see that we are doing four things: creating a product, getting a product from the data store, editing a product, and deleting a product from the data store. We begin by moving the test data that the tests will share to the setUp() method, as follows:

 Product p;
 public void setUp()throws Exception{
 . . .
 . . . p = new Product(){};
 p.setId(1119996);
 p.setFkSubcategoryId(111);
 p.setName("Test1");
 p.setDescription("Test1 Description");
 p.setPrice(11);
 p.setPrice(6); }


Notice that the test object p is now an instance variable, so it can be used in all four tests. The next step is to break the method testCreateDeleteProduct() into four methods. We add the product from the setUp() method:

 public void testCreateProduct() throws Exception { category.createProduct(p);
 }


The next test tests the ability to get the product out of the database. Notice that you could combine this get-product test and the previous test, because this one validates the create-product test:

 public void testGetProduct()throws Exception {
 Product p2 = category.getProduct(1119996);
 assertEquals("name after create",p2.getName(), p.getName());
 }


The next test tests the ability to edit an existing product:

 public void testEditProduct() throws Exception {
 p.setName("Test2");
 category.editProduct(p);
 Product p3 = category.getProduct(1119996);
 assertEquals("name after edit", p3.getName(), p.getName());
 }


Finally, we test the deletion of the product as follows:

 public void testDeleteProduct()throws Exception {
 category.deleteProduct(p.getId());
 Product p4 = category.getProduct(1119996);
 this.assertEquals("proudct should be gone", -1, p4.getId());
 }


You really need to endeavor to keep tests small and as atomic in operation as possible. If something breaks in the future, the test will identify exactly what broke. If we had left the test as it was and something broke, it would be hard to tell what broke—not impossible, but difficult. Plus, these methods describe exactly what we are testing.

Creating an Ant Buildfile to Deploy Our Entity Bean

Just like the Hello World example, the EJB source, libraries, and buildfile are self-contained in their own directory structure:

Java Click To expand

The src directory holds the source files. The jboss-clientlib directory holds the library files needed by JBoss (the EJB server). The META-DATA directory holds the deployment descriptor and the entity property to SQL table field mapping file. The buildfile project name is enterprise_beans; refer to this listing:

<project name="enterprise_beans" default="all" >
 <target unless="setProps" description="setup the properties.">
 <property value="/tmp/petstore" /> <property value="/tools/jboss/jboss/deploy" />
 </target>
 <target depends="setProps" description="initialize the properties.">
 <tstamp/>
 <property value="${outdir}/ejbs" />
 <property value="${ejbout}/ejb-jar" />
 <property value="${ejbout}/ejb-jar-client" />
 <property value="${outdir}/dist" />
 <property value="${outdir}/lib" />
 <property name="meta-data" value="${build}/META-INF" />
 <property name="build_lib" value="./../lib" />
 <property name="jar_name" value="petbeans.jar" />
 </target>
 <target name="clean_jboss" if="jboss">
 <delete file="${jboss}/${jar_name}" />
 </target>
 <target depends="init,clean_jboss" description="clean up the output directories.">
 <delete dir="${build}" />
 <delete dir="${meta-data}" />
 <delete dir="${client}" />
 <delete dir="${dist}/${jar_name}" />
 </target>
 <target depends="init" description="prepare the output directory.">
 <mkdir dir="${build}" />
 <mkdir dir="${lib}" />
 <mkdir dir="${meta-data}" />
 <mkdir dir="${client}" />
 <mkdir dir="${dist}" />
 </target>
 <target depends="prepare" description="compile the Java source.">
 <javac srcdir="./src" destdir="${build}" >
 <classpath >
 <pathelement location="." />

 <fileset dir="${build_lib}">
 <include name="**/*.jar"/>
 </fileset>
 <fileset dir="${lib}">
 <include name="**/*.jar"/>
 </fileset>
 </classpath>
 </javac>
 </target>
 <target name="config_jboss_jndi" if="jboss">
 <copy todir="${client}" > <fileset dir="./jboss_props" />
 </copy>
 </target>
 <target name="config_jndi" depends="config_jboss_jndi" />
 <target depends="compile,config_jndi"
 description="package the Java classes into a jar.">
 <copy todir="${client}" > <fileset dir="${build}" excludes="**/*Bean*" includes="**/*.class*" />
 </copy>
 <jar jarfile="${lib}/client-${jar_name}"
 basedir="${client}" />
 <copy file="./META-DATA/ejb.xml" tofile="${meta-data}/ejb-jar.xml"/>
 <copy file="./META-DATA/jaws.xml" tofile="${meta-data}/jaws.xml" />
 <jar jarfile="${dist}/${jar_name}"
 basedir="${build}" />
 </target>
 <target name="deploy_jboss" depends="package" if="jboss">
 <copy file="${dist}/${jar_name}" todir="${jboss}" />
 <copy todir="${lib}" >
 <fileset dir="./jboss_clientlib" />
 </copy>
 </target>
 <target depends="package,deploy_jboss"
 description="deploys the jar file to the ejb server.">
 </target>
 <target depends="clean,deploy" description="perform all targets."/> </project>


Let's break down the buildfile and explain the important parts step by step. The setProps task defines a property called "jboss", which it sets to the deploy directory of JBoss as follows:

 <target unless="setProps" description="setup the properties.">
 <property value="/tmp/petstore" /> <property value="/tools/jboss/jboss/deploy" />
 </target>


JBoss has a deploy directory; any Enterprise JavaBean copied to the deploy directory will be automatically read and deployed by the JBoss server. The buildfile uses this property to conditionally delete the EJB JAR file during a clean, to copy the JNDI properties needed for JBoss during a package, and to copy the EJB JAR file to the JBoss deploy directory during a deploy, as follows:

 <target name="clean_jboss" if="jboss">
 <delete file="${jboss}/${jar_name}" />
 </target>
 . . .
 <target name="config_jboss_jndi" if="jboss">
 <copy todir="${client}" > <fileset dir="./jboss_props" />
 </copy>
 </target>
 . . .
 <target name="deploy_jboss" depends="package" if="jboss">
 <copy file="${dist}/${jar_name}" todir="${jboss}" />
 <copy todir="${lib}" >
 <fileset dir="./jboss_clientlib" />
 </copy>
 </target>


Obviously, your app server may need extra tasks executed. If you have several app servers to deploy to, you can use this technique to create tasks that are executed conditionally. The compile target of this app is fairly vanilla—that is, it is a lot like the other subprojects. However, the package is interesting because we have to create two JAR files: one for clients and one for the EJB server, as follows:

 <target depends="compile,config_jndi"
 description="package the Java classes into a jar.">
 <copy todir="${client}" > 
 <fileset dir="${build}" excludes="**/*Bean*" includes="**/*.class*" />
 </copy>
 <jar jarfile="${lib}/client-${jar_name}"
 basedir="${client}" />
 <copy file="./META-DATA/ejb.xml" tofile="${meta-data}/ejb-jar.xml"/>
 <copy file="./META-DATA/jaws.xml" tofile="${meta-data}/jaws.xml" />
 <jar jarfile="${dist}/${jar_name}"
 basedir="${build}" />
 </target>


The package task first creates a client-side JAR file by copying the needed files to a staging area and then jarring them. The first step to create a client-side JAR is to use the copy task to copy all the class files except the implementation to a temporary staging directory, as follows:

 <copy todir="${client}" > <fileset dir="${build}" excludes="**/*Bean*" includes="**/*.class*" />
 </copy>


Notice how this copy task uses the excludes pattern **/*Bean* to exclude any class file containing the substring Bean. This step effectively excludes the product implementation class (ProductBean), which is not needed for the client-side JAR. Now that all the needed client-side files are in the ${client} directory, the buildfile can jar the client-side files, as follows:

 <jar jarfile="${lib}/client-${jar_name}"
 basedir="${client}" />


The client JAR file is put in the output directory lib where the Web app buildfile can get it and put it in the WAR file. The JAR file for the EJB server must contain not only the implementation class but also the deployment descriptor and the CMP mappings file. (Note that we copy in a mapping file specific to JBoss. In a real buildfile, you may want to do this in a target that is executed only if the "jboss" property is set.) Here are the tasks to build the server EJB JAR file:

 <copy file="./META-DATA/ejb.xml" tofile="${meta-data}/ejb-jar.xml"/>
 <copy file="./META-DATA/jaws.xml" tofile="${meta-data}/jaws.xml" />
 <jar jarfile="${dist}/${jar_name}" basedir="${build}" />


When this buildfile executes, we get a JAR file in lib (client-petbeans.jar) and a JAR file in the distribution directory (petbeans.jar):

Java Click To expand

In addition to the client-side JAR file, in the case of JBoss the Web app needs the following: naming, RMI, and EJB support libraries (the JAR files ejb.jar, jboss-client.jar, and jbosssx.jar, shown in the following figure. Thus the deploy target depends on the jboss_deploy target, which copies the files to the lib directory where they can be picked up by the Web app buildfile and packaged in the WAR file, as follows:

 <target name="deploy_jboss" depends="package" if="jboss">
 <copy file="${dist}/${jar_name}" todir="${jboss}" />
 <copy todir="${lib}" >
 <fileset dir="./jboss_clientlib" />
 </copy>
 </target>
 <target depends="package,deploy_jboss"
 description="deploys the jar file to the ejb server.">
 </target>


When the WAR file is deployed, the needed libraries are in the WEB-INF/lib directory where they can be used by the Web apps class loader:

Java Click To expand

Now that we have created the classes, tests, and buildfiles to build and deploy the features to add, edit, and delete products, let's update the test buildfile so that it can automatically test the files.

Modifying the Test Buildfile to Test Our Entity Bean

In the following code, we added the lines in bold to the test target of the test buildfile:

 <target depends="compile">
 <junit printsummary="true" fork="yes">
 <formatter type="xml" />
 <test name="test.xptoolkit.model.CategorySystemTest" todir="${reports}" />
 <test name="test.xptoolkit.model.CategoryTest" todir="${reports}" />
 <test name="test.xptoolkit.model.SubcategoryTest" todir="${reports}" />
 <test name="test.xptoolkit.model.ProductTest" todir="${reports}" />

 <test name="test.xptoolkit.petstore.entity.ProductTest" 
 todir="${reports}" />
 <classpath> <fileset dir="${lib}">
 <include name="**/*.jar"/>
 </fileset>
 <fileset dir="${build_lib}">
 <include name="**/*.jar"/>
 </fileset>
 <fileset dir="../EJBeans/jboss_clientlib">
 <include name="**/*.jar"/>
 </fileset>
 <fileset dir="${test_lib}">
 <include name="**/*.jar"/>
 </fileset>
 <fileset dir="/tools/ant/lib">
 <include name="**/*.jar"/>
 </fileset>
 <fileset dir="${build}" />
 </classpath>
 </junit>
 <junitreport todir="${reports}">
 <fileset dir="${reports}">
 <include name="TEST-*.xml"/>
 </fileset>
 <report format="frames" todir="${reports}/html"/>
 </junitreport>
 </target>


Note that we didn't add much, because the additional test methods were added to CategoryTest and CategorySytemTest. The only test we have to add is the entity ProductTest, as follows:

  <test name="test.xptoolkit.petstore.entity.ProductTest" todir="${reports}" />


Because the junitreport task uses a file set, the output from the entity ProductTest is automatically included with the rest. The important point here is that once the testing buildfile is set up, adding new tests and reports is easy. Please look at the output for the entity ProductTest in the following figure.

Java Click To expand

Case Study Conclusion

This case study included test cases used for the baseline version of the pet store. We added an entity bean and Web form to manage product data over the Web (add, delete, and modify product data). Once we added the entity bean, we added test cases to test it, and then we integrated them into the test buildfile. Using this example, we more realistically demonstrated building and deploying an entity bean to our EJB server.


JaVa
Comments