■ Chapter 10—Testing and JDBC
■ Chapter 13—Testing J2EE Applications
11.5 Test CMP meta data outside the container
◆ Problem
You want to test a CMP entity bean, but outside the container.
◆ Background
There was a project on which J. B. worked that used a database overrun with refer- ential integrity. There were foreign key chains—that is, I have a foreign key to you, you have a foreign key to him, who has a foreign key to her, and so on—spanning seven or eight tables. The end result was absolute chaos for testing: to populate the data to test one entity bean required populating dozens of rows in eight differ- ent tables. Not only did this make tests slow to execute, but they were a nightmare to maintain. The team had to choose between isolated test data for each test—and the 45 seconds-per-test execution speed that came with it—and one big suite of test data for a large number of tests, whose cost of change turned out to be aptly modeled by an exponential curve—and a steep one at that. If this sounds like your situation, and if you absolutely must use entity beans, and if you absolutely cannot do away with a majority of those referential integrity constraints,16 then you need another plan. This recipe is your other plan.
15Ed Roman, Scott Ambler and Tyler Jewell. Mastering Enterprise JavaBeans, 2nd Edition. John Wiley & Sons, 2001. This book is freely available online at www.theserverside.com/books/wiley/masteringEJB/index.tss.
16Show this sentence to the nearest Database Administrator. He may find it funny... or upsetting. Either way, you are sure to get a reaction.
TE AM FL Y
Team-Fly®
401 Test CMP meta data
outside the container
◆ Recipe
If testing the entity bean in a live container is taking too much time, then what you can do is test the entity bean meta data instead. The good news is that this meta data is typically expressed in XML, so it is quite easy to test. For a typical CMP entity bean, you can test any of the following without the container:
■ The container-managed fields are specified correctly.
■ The mapping between the entity bean class and a database table is correct.
■ The mapping between container-managed fields and table columns is correct.
■ A table column mapping exists for each container-managed field.
■ The entity bean uses the correct data source.
■ EJBQL queries are correctly specified.
■ Container-managed relationships are correctly specified.
There are probably others, but this list is a good place to start. You can use XML- Unit (see chapter 9, “Testing and XML”) to write tests for all the various deploy- ment descriptors and server configuration files. Note that the way you test your server configuration depends on how your application server stores that informa- tion. For example, JBoss stores it all in XML documents, making it easy to test with XMLUnit. Specifically, we could test the mapping between our CoffeeCatalogItem entity bean, representing an item in the Coffee Shop’s catalog, and the catalog.
beans database table that provides persistent storage for it. Listing 11.7 shows the test we would write for JBoss 3.2.4.17 Note that it extends XMLTestCase, part of the XMLUnit package.
package junit.cookbook.coffee.model.ejb.test;
import java.io.FileReader;
import org.custommonkey.xmlunit.*;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
public class CoffeeCatalogItemEntityBeanMetaDataTest extends XMLTestCase {
17This test uses an XML document that declares a DTD, so you’ll either need a network connection to execute it, or you’ll have to edit the XML document to point to your local copy of the JBoss DTD in question.
Listing 11.7 CoffeeCatalogItemEntityBeanMetaDataTest
402 CHAPTER 11
Testing Enterprise JavaBeans
private static final String META_DATA_FILENAME =
"../CoffeeShopEJB/ejbModule/META-INF/jbosscmp-jdbc.xml";
private static final String ENTITY_BEAN_XPATH = "/jbosscmp-jdbc/enterprise-beans/"
+ "entity[ejb-name='CoffeeCatalogItem']/";
private Document metaDataDocument;
protected void setUp() throws Exception { XMLUnit.setIgnoreWhitespace(true);
metaDataDocument =
XMLUnit.buildTestDocument(
new InputSource(new FileReader(META_DATA_FILENAME)));
}
public void testTableMapping() throws Exception { assertXpathEvaluatesTo(
"catalog.beans",
ENTITY_BEAN_XPATH + "table-name", metaDataDocument);
}
public void testFieldMapping() throws Exception { assertXpathEvaluatesTo(
"productId", ENTITY_BEAN_XPATH
+ "cmp-field[field-name='productId']/column-name", metaDataDocument);
assertXpathEvaluatesTo(
"coffeeName", ENTITY_BEAN_XPATH
+ "cmp-field[field-name='coffeeName']/column-name", metaDataDocument);
assertXpathEvaluatesTo(
"unitPrice", ENTITY_BEAN_XPATH
+ "cmp-field[field-name='unitPrice']/column-name", metaDataDocument);
}
public void testDataSource() throws Exception { assertXpathEvaluatesTo(
"java:/jdbc/mimer/CoffeeShopData", "/jbosscmp-jdbc/defaults/datasource", metaDataDocument);
} }
403 Test CMP meta data
outside the container
All the XPath expressions in this test come from reading the Document Type Defi- nition (DTD) for configuring container-managed persistence for JBoss. You need to consult the corresponding documentation for your application server of choice to obtain the same results—and that assumes that your vendor stores that informa- tion in XML as JBoss does. For those of you using JBoss, after you write this test, you might not be clear what the XPath expressions in the test mean. We decided to apply a few refactorings to this example. Here is a summary of what we did:
1 We revealed the intent behind the XPath expressions by introducing appro- priately named methods.
2 We removed duplication in the XPath-based assertions, particularly involv- ing substrings that the XPath expressions have in common.
3 We introduced a new class named EntityBeanMetadataTest and made it part of Diasparsoft Toolkit.18
4 We pulled up [Refactoring, 322] all the newly created, generic methods into the new class, leaving only the original tests behind, and with much less “noise” to distract the programmer.
The end result is the test case class in listing 11.8. Notice that setUp() invokes setEntityBeanUnderTest() so that the tests do not need to duplicate the name of the entity bean under test throughout. We tend to test each entity bean in its own test fixture, so this makes the most sense to us at the moment. We have shown the changes highlighted in bold print.
package junit.cookbook.coffee.model.ejb.test;
import java.io.FileReader;
import javax.xml.transform.TransformerException;
import org.custommonkey.xmlunit.XMLUnit;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import com.diasparsoftware.util.jboss.testing.EntityBeanMetaDataTest;
public class CoffeeCatalogItemEntityBeanMetaDataTest extends EntityBeanMetaDataTest {
protected void setUp() throws Exception { setMetaDataFilename(
"../CoffeeShopEJB/ejbModule/META-INF/jbosscmp-jdbc.xml");
setEntityBeanUnderTest("CoffeeCatalogItem");
18 www.diasparsoftware.com/toolkit
Listing 11.8 The refactored test
404 CHAPTER 11
Testing Enterprise JavaBeans
super.setUp();
}
public void testTableMapping() throws Exception { assertBeanMappedToTable("catalog.beans");
}
public void testFieldMapping() throws Exception { assertFieldMappedToColumn("productId", "productId");
assertFieldMappedToColumn("coffeeName", "coffeeName");
assertFieldMappedToColumn("unitPrice", "unitPrice");
}
public void testDataSource() throws Exception {
assertDefaultDataSource("java:/jdbc/mimer/CoffeeShopData");
} }
What about EntityBeanMetaDataTest? Listing 11.9 shows an early version of this class. It will evolve over time to meet the needs of whoever might use it. As you write tests for other types of J2EE meta data, you might find yourself extracting classes [Refactoring, 149] similar to this one. If you do, please make those classes available to the rest of the J2EE programming community. We would appreciate it!
package com.diasparsoftware.util.jboss.testing;
import java.io.FileReader;
import javax.xml.transform.TransformerException;
import org.custommonkey.xmlunit.*;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
public abstract class EntityBeanMetaDataTest extends XMLTestCase { private String metaDataFilename;
private Document metaDataDocument;
private String entityBeanName;
protected void setUp() throws Exception { parseMetaData();
}
protected void setMetaDataFilename(String metaDataFilename) { this.metaDataFilename = metaDataFilename;
}
protected void setEntityBeanUnderTest(String entityBeanName) { this.entityBeanName = entityBeanName;
}
protected void parseMetaData() throws Exception { Listing 11.9 An early version of EntityBeanMetaDataTest
405 Test CMP meta data
outside the container
XMLUnit.setIgnoreWhitespace(true);
metaDataDocument =
XMLUnit.buildTestDocument(
new InputSource(new FileReader(metaDataFilename)));
}
protected void assertBeanMappedToTable(String expectedTableName) throws TransformerException {
assertXpathEvaluatesTo(
expectedTableName,
getXpathRelativeToEntityBean(entityBeanName, "table-name"), metaDataDocument);
}
protected void assertFieldMappedToColumn(
String fieldName,
String expectedColumnName) throws TransformerException { assertXpathEvaluatesTo(
expectedColumnName,
getColumnMappingForField(entityBeanName, fieldName), metaDataDocument);
}
protected void assertDefaultDataSource(
String expectedDataSourceJndiName) throws TransformerException { assertXpathEvaluatesTo(
expectedDataSourceJndiName,
"/jbosscmp-jdbc/defaults/datasource", metaDataDocument);
}
private String getColumnMappingForField(
String entityBeanName, String fieldName) {
return getXpathRelativeToEntityBean(
entityBeanName,
"cmp-field[field-name='" + fieldName + "']/column-name");
}
private String getXpathRelativeToEntityBean(
String entityBeanName, String relativeXpath) {
return getEntityBeanXpath(entityBeanName) + relativeXpath;
}
private String getEntityBeanXpath(String entityBeanName) { return "/jbosscmp-jdbc/enterprise-beans/"
+ "entity[ejb-name='"
406 CHAPTER 11
Testing Enterprise JavaBeans
+ entityBeanName + "']/";
} }
◆ Discussion
We should emphasize at this point that implementing referential integrity in the database is not necessarily everything people claim it is. In fairly specific circum- stances it is more of a hindrance than an aid. In particular, if there is only one application accessing the database, then it is possible to place all referential integ- rity rules in the application and leave them out of the database. Aside from purist arguments against this practice, it makes testing easier by reducing the size and complexity of the data sets you need for testing. This is a sizable benefit and should not be discounted right away. Referential integrity constraints do help keep invalid data out of the database—data which could come from places entirely out of your control—but they come at a cost, and you need to balance that cost against the benefits. There is no free lunch. (See the post script to this recipe for an opposing view.)
Another way to reduce the cost of testing entity beans against a live database is to execute those tests (and only those tests) against a version of the database schema without referential integrity rules. This gives you the best of both worlds:
the safeguard of referential integrity in production without the shackles of too much referential integrity during testing. If you choose this direction, do be care- ful of any defects that “slip through” as a result. If you find that this strategy leads to defects that only occur because you are using the strategy, then either rethink the strategy or try to learn from your mistakes. Every benefit has its price.
You might wonder about using this technique to test EJBQL queries; after all, the only way to be certain that an EJB query works is to try it against a live applica- tion server. We agree with this, but that does not necessarily make it a good idea to test every query against a live application server. This is another case where you need to balance cost and benefit. One maxim among test-oriented programmers (such as those who practice Test-Driven Development) is “test until fear turns to boredom.” That is, keep writing tests until you are confident that the code works, then stop. On the other hand, if you are afraid that the code does not work, then keep writing tests until the fear subsides. When we follow this guideline we tend to work in cycles: we test everything down to the smallest detail, and then boredom (even complacency!) sets in, we relax our guard, a defect pops up that embar- rasses us, and we turn the testing knob back up to 11. This appears to hit the sweet spot in the trade-off between the cost and benefit of testing.
407 Test CMP meta data
outside the container
You should apply this principle to testing EJBQL queries. You want to minimize the number of these tests and execute them less frequently, such as in a back- ground continuous build process using Cruise Control or Anthill. At the begin- ning, write in-container tests for all EJBQL queries. Over time, as you become more comfortable, write fewer tests for those queries; however, and this is the important part, whenever you introduce a defect through an EJBQL query you must write a test for it. Without this important feedback, you will begin to feel as though you never make a mistake, and if you truly felt that way, then we would wonder why you need this book!19 Remember that the goal is still to produce defect-free code, so if you introduce a defect because you forgot to write a test, then write the test. Over time, this will occur less frequently. When a problem arises, your End-to-End Tests will catch them (or QA if you are less lucky) and alert you to the problem. You can then execute your in-container tests to isolate the problem and help you fix it.
◆ Post script
Our intrepid reviewer, George Latkiewicz, dislikes databases without referential integrity constraints. “If I had a choice I wouldn’t shake a stick at a database that didn’t have RI. I have personally worked on a project where a whole team spent over a year attempting to reverse engineer the RI that should have been defined in a DBMS and cleaning up the data that violated those constraints (‘What do we do when there is no matching person for the transaction and no matching prod- uct, but the accounting information records that the amount was actually paid?’
and ‘Can you figure out the person from the credit card number?’). Literally mil- lions of dollars and thousands of hours spent because of a handful of missing RI constraints.” Obviously we ought to take George’s experience to heart, but we tend to take an extreme position on these issues because that is usually when we learn the most, and the more we know, the better for our clients. Here, we ask,
“How much are these constraints helping us, anyway? Let us get rid of them all and see.” We have worked on several projects that use referential integrity spar- ingly, if at all, and have walked away from them unharmed. Whose position is bet- ter—George’s or ours? As usual, even better would be someplace in between, which is why we recommend you try both and measure the results. If nothing else, this recipe reminds us not to bear the cost of referential integrity (or anything else, for that matter) without understanding the benefit.
◆ Related
■ Chapter 9—Testing and XML
408 CHAPTER 11
Testing Enterprise JavaBeans