Test infect your Enterprise JavaBeans

Learn how to test your J2EE components live and in the wild

We can never overtest software, but we seldom test it enough. When testing does begin, system or integration testing is the norm, with formal testing and quality assurance efforts typically starting at the last minute -- when the software is nearing release. Inevitably, pressure builds on testers to rush through the testing process. Needless to say, this does not encourage thoroughness. At the same time, the development team is urged to fix defects as quickly as possible. This, too, does not promote careful attention to detail. When this process does produce high-quality software -- a rare occurrence -- it is the result of superhuman efforts by conscientious individuals.

Since a J2EE application is assembled from components, integration testing comes too late in the process. Individual components often behave differently when they are by themselves than when assembled with other components. To eliminate unexpected interactions, J2EE components must be unit tested before they are gathered into an application. This article discusses unit testing techniques and how to apply unit testing to J2EE components, particularly Enterprise JavaBeans (EJBs).

Note: The complete source code for this article, including JUnit 3.2 sources, the servlet interface, build scripts, the deployment descriptor, and the sample tests can be downloaded as a zip file in Resources.

Unit testing

If you are already test infected, skip to the next section. If not, you need to know about the benefits of real unit testing.

Unit testing is a critical, but often misunderstood, part of the software development process. Unit testing involves individually testing each small unit of code to ensure that it works on its own, independent of the other units. In object-oriented languages, a unit is often, but not always, equivalent to a class.

If developers knew for certain that each piece of the application works as it was designed to do, they would realize that problems with the assembled application must result from the way the components were put together. Unit testing tells developers that an application's pieces are working as designed.

For example, if you were building a car, you would probably construct each of its many complex components separately. Unless you tested each piece individually before assembly, you would have a lot of trouble figuring out why the car doesn't run correctly after it is put it together. For example, was the transmission built improperly, or did you hook it up incorrectly? Without having evaluated each piece beforehand, you have no way of knowing whether one or more of the pieces was built incorrectly, whether your integration was faulty, or both. Imagine the amount of time you would waste trying to analyze what was wrong with the car. And how much confidence would you have in its long-term reliability? If you had tested each component by itself before assembly, you would be able to focus your debugging efforts on the integration -- the way you assembled the car. And since you would have confidence in the individual pieces of the car, you would have more confidence in the car as a whole.

For the same reasons, it is important to unit test software. But, just as you wouldn't test the car parts without test-driving the completed car, you must also follow unit testing with integration testing and functional testing. Unit testing is just one part of a good, solid software development process.

Unit tests are typically organized into groups or suites of tests. A project's unit test suites should always execute successfully at 100 percent. Any failure in the unit tests instantly becomes a top priority for responsible developers.

These unit tests are useful for programmers long before deployment. A wide-reaching unit test suite bolsters confidence by allowing developers to change the code without fear of breaking it. Any change that would break the code will immediately be caught by a unit test that fails. Also, though unit testing does not focus on code coverage, developers know which parts of the code are most likely to break and will test those parts more rigorously.

Unit testing at this level also catches problems long before any other testing method will reveal the defects. Developers will usually run the unit tests every 10 or 15 minutes. When a test fails, developers must debug only the last 10 minutes' worth of changes at any one time. With other testing methods, developers sometimes must debug changes made over the span of several months.

Unit testing allows developers to start writing tests before writing the code.

The process works this way:

  • Decide what the component should do.
  • Design the component. This can be done formally or informally, depending on the complexity of the component.
  • Write unit tests to confirm that behavior. At first, the tests will not compile, since the code they test is not yet written. Your focus here is on capturing the intent of the component, not on the implementation.
  • Start coding the component to the design. Refactor as needed.
  • When the tests pass, stop coding.
  • Consider other ways the component can break; write tests to confirm and then fix the code.
  • Each time a defect is reported, write a test to confirm. Then fix the code.
  • Each time you change the code, rerun all tests to make sure you haven't broken anything.

By writing the test cases before producing the code, you gain clarity of purpose. This system lets you understand exactly what the requirements for that component are because you just captured those requirements in code. You also gain a unique opportunity to explore the requirements. For example, suppose you are asked to create a string parsing utility. Your requirements might specify certain tasks the string parser must perform, but they might not specify what the parser should do with an empty or null string. If you don't consider this detail while creating the component, a nasty and a difficult-to-trace NullPointerException could result at runtime. By writing the test cases in advance, you will take into account items that could cause the component to fail.

In addition to helping clarify the requirements, your test cases prevent you from overdesigning or complicating your implementation. After all, when the tests run, you simply stop coding. You also gain the freedom to change your classes more confidently. Instead of wishing you could change a particular piece of code and being unsure what effect it would have on the system, you can now make your changes without worry. Your tests will tell you what the impact of your changes is.

Maintenance also benefits from unit tests. It is often easier to figure out a component's expected use by reading its tests than by reading the object itself. As maintenance developers make changes, they use the unit tests, which give instant feedback. Regression testing doesn't have to take place immediately before release; you can test continually -- every 10 or 15 minutes. You have captured the requirements for your code in the one place guaranteed to live as long as the code -- the code itself.

The JUnit framework, written by Erich Gamma and Kent Beck, enables comprehensive unit testing by making it easy -- even fun -- to write test code. With JUnit, each test case extends the TestCase class. Any public, no-argument method in which the name starts with "test" is executed one at a time. The test methods call the component under test and make one or more assertions about the behavior of that component. JUnit reports the precise location of each failed assertion.

As an example, let's think about a simple telephone number object. What behavior should it have? At a minimum, it will have accessors for the components of a U.S. number: area code, exchange, number, and extension. We should also have a formatter that gives a nice string representation. Remember, we write the test first.

The following idiom starts a test case that can run by itself or be incorporated into a larger suite of tests:

import junit.framework.*;
public class TelephoneNumberTests extends TestCase {
   public static void main(String[] args) {
      junit.textui.TestRunner.run(suite());
   }
   public static TestSuite suite() {
      return new TestSuite(TelephoneNumberTests.class);
   }
}

This test will execute. Since there are no test methods, it reports an error. What do we need to add for test methods? The rule of thumb is: If it can possibly fail, write a test for it. There are some exceptions to this rule, however. For example, since simple accessor methods rarely fail, they rarely are tested. Of course, if your accessor does more than just set an instance variable, you should write a test for it. Choosing precisely what to test takes some practice.

Here is a test method for the string formatting:

   public void testSimpleStringFormatting() throws Exception {
      // Build a complete phone number
      TelephoneNumber number = new TelephoneNumber("612", "630", "1063",
"1623");
      assertEquals("Bad string", "(612) 630-1063 x1623",
number.formatNumber());
   }

The assertEquals() method takes a message string, the expected value, and the actual value. Internally, if expected.equals(actual) returns false, the assertion fails.

This method verifies the basic case, but we can think of many ways for string formatting to fail. What happens if any or all of the parts are null? toString() must do something reasonable. Let's say the area code and extension are optional, but the exchange and number must be present or a NullPointerException will be thrown. Notice that we are defining the contract of the TelephoneNumber object well before writing the object itself. We are focused on its expected behavior, not on what is easiest to implement.

Here are the new test methods:

   public void testNullAreaCode() throws Exception {
      // Build a phone number without area code
      TelephoneNumber number = new TelephoneNumber(null, "630", "1063",
"1623");
      assertEquals("Bad string", "630-1063 x1623", number.toString());
   }
   public void testNullExtension() throws Exception {
      // Build a phone number without an extension
      TelephoneNumber number = new TelephoneNumber("612", "630", "1063", null);
      assertEquals("Bad string", "(612) 630-1063", number.toString());
   }
   public void testNullAreaCodeAndExtension() throws Exception {
      // Build a phone number without area code or extension
      TelephoneNumber number = new TelephoneNumber(null, "630", "1063", null);
      assertEquals("Bad string", "630-1063", number.toString());
   }
   public void testNullExchange() throws Exception {
      // Build a phone number without exchange
      TelephoneNumber number = new TelephoneNumber("612", null, "1063",
"1623");
      try {
         number.toString();
         assert("Should have thrown a NullPointerException", false);
      } catch(NullPointerException npe) {
         // expected behavior
      }
   }

(You can find the complete code for TelephoneNumberTests.java by downloading jw-0526-testinfect.zip in Resources.)

These tests define the behavior of the TelephoneNumber. You can probably come up with more ways to break the object, as it's pretty loosely defined right now. You can also see that implementing the TelephoneNumber object will be somewhat mechanical once it's specified by the tests.

The problem with unit testing EJBs

Now that you are sold on the benefits of unit testing, you must be warned about one problem: Unit tests run best when they run individually, in isolation, and quickly. A test case typically constructs the objects it is testing, as shown in the example above. However, sometimes the object being tested is dependent on the behavior of other objects. In such situations, the test case also builds a test harness to "stub out" the needed behavior. Such a test harness has the same interface as the real system, but doesn't actually do anything. For example, a test case for a robot arm might provide objects that behave like a motor and a position sensor. These would fool the unit being tested into thinking it was calling the real robot arm, even though nothing is moving. Although this works well for most standalone applications, it becomes outrageously complicated when you test code -- like an EJB -- that runs in an application server.

Indeed, EJBs are different. They are not meant to be constructed directly by their callers. They usually rely on context provided by their container, and without that context, most EJBs will not function. Although, conceivably, a test case could construct a convincing test harness, most likely the harness would be as complicated as the application server itself. In addition, you would have to test the test harness. And, since you've just built an application server, you might as well deploy your application on it. Otherwise, the testing and deployment environments would be different.

This problem also exists for other types of objects. Testing an RMI object, servlet, or any other component that gains context from its application server is just as difficult. If testing the object outside its native environment is even possible, the validity of the test might be compromised. Unit testing on the application server can be used for any of these cantankerous components.

1 2 Page
Recommended
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more