ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Automating EJB Unit Testing

by JiRong Hu
02/05/2003

Testing EJB is different

Container matters

Enterprise Java Beans, or EJBs, cannot be tested on their own as can plain Java classes. There are additional steps to deploy them to an EJB container before they can be tested. This means that our testing process must include the additional process of deployment and re-deployment of EJBs. The deployment must be automated as well.

There are some arguments on the differences between running the EJB test client inside or outside of the EJB container. If you write your EJB test code with JUnit and run the test client directly from your IDE, or even from the command line, that is an outside container testing. On the other hand, you can use other test frameworks -- such as Apache Cactus -- to simulate in-container testing. There are also other similar approaches, such as a servlet interface, as introduced in the article "Test Infect Your Enterprise Java Beans" (see Resources).

Although using a server-side testing framework to test EJBs looks more comfortable, from our experience in terms of unit testing, the outside container testing is safe enough for programmers to prove the beans are still working as expected during development. That is all we want in Extreme Programming, or XP. (We will elaborate our scope of unit testing in the next section.) We have not encountered any problems regarding this issue; in fact, we appreciate its simplicity and speed.

Database Matters

Entity beans are actually data objects. Entity bean manipulations such as creating new entity beans, updating entity beans or deleting entity beans, are eventually converted to CRUD database access code, producing the same results as using direct JDBC access. This produces a few issues with the test data.

Duplicate records

You cannot have duplication data for primary key columns when testing entity beans, otherwise the database's intrinsic mechanism will reject the data. (This can be a test, though.) You must design your test suite to avoid this situation.

Related Reading

Java Enterprise Best Practices
By The O'Reilly Java Authors

Dependent data

In real life, most of data in the database has relationships. Creating new data may require some dependent data to exist. Deleting data with dependencies may not be allowed.

Large data

Sometimes the data set is really big.

Visual Inspection

When developing EJBs, most programmers will check the result of testing. For example, the creation of a new entity bean may be tested by visually inspecting the database. This is slow and inaccurate.

This article presents a few techniques to avoid those problems by properly designing initial test data and by using simple JDBC access code.

The Scope of Unit Testing

Automated unit testing is a foundation practice in XP. Without this, it is almost impossible to practice other behaviors, such as continuous integration and aggressive refactoring. Before we propose our solution, we want to give a scope for the unit testing in our context. When we say unit testing as in the title of this article, we actually mean a little bit more than the original rigorous definition of unit testing, which is commonly limited to the class level in Java.

In XP, what we really want to achieve is to make sure everything still works properly during continuous integration and after aggressive refactoring. In the context of EJB testing, our unit testing will cover not only simple methods inside a bean (entity bean and session bean), but also small subsystems such as a session (façade) bean. In the latter case, multiple entity beans are accessed and more than one function may be tested in a simple unit test case. In our opinion, a session bean with several highly cohesive entity beans forms a good unit, making it a good candidate for the assembled unit for unit testing.

In this case, we agree on the new definition given by Sheldon Wosnick from IBM in his article on Cactus (see Resources), integration unit testing -- "a compromise between code logic unit testing and function unit testing," in his words. We will not discuss test-first or test-driven techniques in this article. Comprehensive resources on unit testing can be found at www.junit.org .

Two Scenarios

There are two common scenarios where EJB unit testing may be performed: during development and during integration. By "development," we mean at the time of coding. The developer may want to run the unit test at any time after some code changes. This testing is normally performed on one or a few beans at a time.

By "integration," we mean at the time beans from different developers are integrated together and integration test suites are run, since beans may behave differently when assembled together. This is normally adopted by XP teams from the idea of continuous integration. Integration tests may be run every day or even every hour.

We will focus on development unit testing in this article. However, the idea and even the same code can be used in integration tests. For example, selective session bean test cases can be used in an integration test suite. The best part of this approach is that you don't even need two sets of test suites for unit testing and integration testing, in the case of EJB testing.

Automating Deployment Process

In both development unit testing and integration unit testing, beans must be redeployed before performing another round of testing. This is the process of removing the deployed bean from a container and redeploying it. There is also a technology called Hot Deploy. Unfortunately, not all EJB containers have implemented this feature well.

For development unit tests, automating the deployment process may not be necessary, since the tests normally just modify one or a few related beans. A manual redeployment may be faster.

However, in integration unit testing, a complete and fully automated deployment process is crucial to the whole automation process. Without that, the continuous integration in EJB development is not possible. All of the EJB container providers provide some kind of command-line tools for the EJB deployment, e.g., the XMLConfig for IBM WebSphere. With the help of Apache Ant, you can organize perfect scripts to automate this deployment. We are not going to elaborate on this subject, since the tools from different vendors are quite unique and the industry has not yet produced a standard.

The ACL Sample

There is nothing more convincible in XP than running sample code. In this article, we choose the Access Control List sample from the book The Art of Objects (see Resources), since it has enough complexity to represent our ideas on EJB unit testing but is easy to understand. Here is a fine-grained access control system implemented by a binary association class pattern.


Figure 1

The class ACL is associated with multiple users and groups and has has associated classes UserAccess and GroupAccess. The Privilege class specifies the privilege for UserAccess or GroupAccess (such as read-only, read-write, and so on). A User can belong to more than one Group. Under an ACL object, there may be many users and groups. The ACL class will have the operation isPrivilegeGranted(user : User, requestedPrivilege : Privilege). Certain policies may be embedded in the operation. For example:

To implement the above model in EJB, we will have as entity beans User, Group, UserAccess, GroupAccess, ACL, and Privilege, and a session bean of ACLManager. Figure 2 shows a simple database ER diagram. You can find SQL scripts for DB2 in the attached source code to this article, as well as all of the Java source code in the EAR file generated from IBM WebSphere Studio Application Developer version 5.


Figure 2

Automating Entity Bean Unit Testing

To Be a True Unit Test

Unit testing is best kept at the class level. Generally, unit testing code should test only one EJB at a time. This rule applies well to Entity Bean testing. In ACL, each entity bean has its own unit testing class. For example, the TestGroup class tests the Group Entity Bean. In addition, the test cases in TestGroup only test the Group bean's functions, and none from other beans.

public static Test suite() {
   TestSuite suite = new TestSuite();
   suite.addTest(new TestGroup("testCreateGroup"));
   suite.addTest(new TestGroup("testFindGroupByGroupId"));
   suite.addTest(new TestGroup("testFindAllGroups"));
   suite.addTest(new TestGroup("testDeleteGroupByGroupId"));
   return suite;
}

List 1: JUnit Test Suite for Class TestGroup

Create the Dependent Data

In almost any real product, classes have all kinds of relationships. In our ACL example, a User always has a Group, so we must have initial Group data before we can test User. There are two ways we can go with that: create the dependent data on the fly inside the test program, or create initial data in the database. In our projects, we have a complete command line script to create database and initial test data. They are in ClearCase and shared with every programmer. Each programmer uses this script to create his own development database on his PC.

The first solution has some drawbacks:

  1. Sometimes the bean you want to test could depend on much data, e.g. the UserAccess bean will need initial data from User, Privilege, and ACL. Setting up the initial data will be quite a job. If every programmer has to do this for every bean, the accumulated effort is unacceptable from our experience.
  2. Error propagations; e.g., if you have an error when creating the Group bean, all of the subsequent tests on User and UserAccess will fail.
  3. Time-consuming; the initial data setup though EJB takes time. This contradicts one of the unit testing rules: be fast.

We find that the latter option is normally better. Here is a sample (all of the scripts are provided in the download package):

INSERT INTO GROUP (ID, NAME) VALUES ('AdminGroup', 'adminstrator');
INSERT INTO GROUP (ID, NAME) VALUES ('DevGroup', 'developer');
INSERT INTO GROUP (ID, NAME) VALUES ('OutGroup', 'outsider');

INSERT INTO USER (ID, NAME, GROUP_ID) VALUES
   ('AdminUser', 'administrator1', 'AdminGroup');
INSERT INTO USER (ID, NAME, GROUP_ID) VALUES
   ('DevUser1', 'developer1', 'DevGroup');
INSERT INTO USER (ID, NAME, GROUP_ID) VALUES
   ('DevUser2', 'developer2', 'DevGroup');
INSERT INTO USER (ID, NAME, GROUP_ID) VALUES
   ('ArchUser', 'architect', 'DevGroup');
INSERT INTO USER (ID, NAME, GROUP_ID) VALUES
   ('OutUser', 'outsider', 'OutGroup');

INSERT INTO USER_GROUP(USER_ID, GROUP_ID) VALUES('AdminUser','AdminGroup');
INSERT INTO USER_GROUP(USER_ID, GROUP_ID) VALUES('DevUser1','DevGroup');
INSERT INTO USER_GROUP(USER_ID, GROUP_ID) VALUES('DevUser2','DevGroup');
INSERT INTO USER_GROUP(USER_ID, GROUP_ID) VALUES('ArchUser','DevGroup');
INSERT INTO USER_GROUP(USER_ID, GROUP_ID) VALUES('OutUser','OutGroup');
INSERT INTO USER_GROUP(USER_ID, GROUP_ID) VALUES('ArchUser','AdminGroup');

INSERT INTO ACL(ID, DESC) VALUES('File', '');

INSERT INTO PRIVILEGE (ID, DESC) VALUES (0, 'NO_ACCESS');
INSERT INTO PRIVILEGE (ID, DESC) VALUES (1, 'READ_ONLY');
INSERT INTO PRIVILEGE (ID, DESC) VALUES (2, 'READ_WRITE');
INSERT INTO PRIVILEGE (ID, DESC) VALUES (3, 'FULL_CONTROL');

INSERT INTO USER_ACCESS(ID, PV_ID, USER_ID, ACL_ID) VALUES
   ('AdminUserAccess',2,'AdminUser','File');
INSERT INTO USER_ACCESS(ID, PV_ID, USER_ID, ACL_ID) VALUES
   ('DevUserAccess1',1,'DevUser1','File');
INSERT INTO USER_ACCESS(ID, PV_ID, USER_ID, ACL_ID) VALUES
   ('DevUserAccess2',1,'DevUser2','File');
INSERT INTO USER_ACCESS(ID, PV_ID, USER_ID, ACL_ID) VALUES
   ('OutUserAccess',0,'OutUser','File');

INSERT INTO GROUP_ACCESS(ID, PV_ID, GROUP_ID, ACL_ID) VALUES
   ('AdminGrpAccess',2,'AdminGroup','File');
INSERT INTO GROUP_ACCESS(ID, PV_ID, GROUP_ID, ACL_ID) VALUES
   ('DevGroupAccess',1,'DevGroup','File');
INSERT INTO GROUP_ACCESS(ID, PV_ID, GROUP_ID, ACL_ID) VALUES
   ('OutGroupAccess',0,'OutGroup','File');

List 2 -- create_test_data.sql

Patterns of Test Data

We will usually find test data two places: the initial test data in the database as we discussed in the previous section, and the data we used in our test program; e.g., the class TestGroup.

Here we suggest to use a different pattern of test data for initial data in the database from the one the developer used inside of the actual test program. For example, in ACL, the initial data in the database conforms to the Java class name convention style, such as "AdminGroup" and "AdminUser." We use all upper case with underscore (database convention) style, such as "GROUP_1" and "USER_1," in the test program. You can use any style you like. The point is: by having completely different data patterns, you will avoid conflicts. Without this mechanism, the test programmer can easily create the same data which is already inside the database, so that you will get the DuplicateKeyException frequently.

Data-Driven Testing Implementation

It is always a good idea to use static final classes to contain the fixed data instead of hard-coding them in the program. In ACL, we create a non-instantiable utility class, TestData, to serve this purpose.

public class TestData {
   private TestData() {} // prevent instantiation
   /**
   * test data for Group
   **/
   // data to be used in code
   public static final String GROUP_1 = "GROUP_1";
   public static final String GROUP_2 = "GROUP_2";
   public static final String GROUP_3 = "GROUP_3";

   // initial data in the database
   public static final String ADMIN_GROUP = "AdminGroup";
   public static final String DEV_GROUP = "DevGroup";
   public static final String OUT_GROUP = "OutGroup";

   // ...
}

Notice we have put in both database initial data and test program data to make the maintenance of those data easier.

To extend this solution and make it more flexible, you can put the test data in XML files and read them at runtime. JXUnit does exactly this. There are other techniques you can use as well and they are covered under the topic of Data-Driven Testing.

Verify the Result

Visual inspection is a manual process; it breaks automation, so we have to fix it. There are wide variety of assert methods in JUnit for us to use to validate the test results for EJBs. For example, after creating a new entity bean, assertion functions can be used to check the identicality of the data in the database. We can retrieve the data from the database with simple SQL statements, comparing it to the test data. In practice, a well-designed JDBCFramework (see Resources) can be a lot of help in executing SQL statements. The following code snippet creates a new Group and retrieves the data with JDBCFramework before comparing it to our test data.

public void testCreateGroup() throws Exception {
   try {
      groupHome.create(TestData.GROUP_1, TestData.GROUP_1);
      System.out.println(TestData.GROUP_1 + " created");
      // verify the result
      GroupDTO group = GroupSqlHelper.getGroup(TestData.GROUP_1);
      assertEquals(group.getName(), TestData.GROUP_1);
   } catch (Exception e) {
      // clean the db in case of error, e.g. duplicated key
      GroupSqlHelper.deleteGroup(TestData.GROUP_1);
   }
}

Generally, the test code does not need to catch application exceptions. JUnit will automatically consider uncaught exceptions as errors. In the above sample, we catch the exception to make sure the database is clean after executing the test program.

The JDBCFramework is not only useful in the unit testing, but actually imperative in EJB development. Using straight JDBC instead of entity beans for bulk reading and deleting is a common practice in EJB development. For example, it is better to use direct JDBC calls to delete all records in the database instead of finding all of the entity beans to remove them one by one.

public void deleteAllGroups() throws Exception {
   //implemented by JDBCFramework
   GroupSqlHelper.deleteAllGroups();
}

Leave Nothing

A unit test should always restore the original state of the system after it finishes -- the state of the system should be exactly the same before and after you run your unit test. This makes sure any of the test programs only depend on and solely rely on the initial state (data) of the testing system. Changing the system state in one test program in the middle could make other tests fail, preventing a successful automation.

When it comes to the actual implementation, in case of errors during testing, your test code should make sure that the system state is predictable. That is, you must clean up after yourself when your test finishes or crashes. However the test code is run, the database must be clean to allow subsequent running of other tests. The database must be restored to the initial state before the next test is run. This means at least two things:

The JDBCFramework can come to our rescue again. For example, the following code snippet calls the deleteGroup() method to delete the record in case the EJB remove test code fails. If you are modifying a record, an update method can help to restore the initial database values.

public void testDeleteGroupByGroupId() throws Exception {
   try {
      GroupKey key1 = new GroupKey(TestData.GROUP_1);
      groupHome.remove(key1);
   } catch (Exception e) {
      // do a manatory remove to make sure db is clean
      GroupSqlHelper.deleteGroup(TestData.GROUP_1);
   }
   assertNull(GroupSqlHelper.getGroup(TestData.GROUP_1));
}

Unit Testing Session Beans

Session beans are used more or less as façades in EJB systems. A session bean can implement a collection manager pattern such as UserManager for entity bean User, GroupManager for Group, or a façade to a subsystem that provides business logic, such as ACLManager for ACL.

In the first case, all methods in the session bean are delegated to corresponding entity beans so that the tests applied to the entity bean apply to the session bean as well.

In the second case, if the methods in the session bean modify the database (and you can tell this, right?) the rules that apply to the entity bean should apply as well. But if the particular method you are testing is not modifying the database; e.g., the isPrivilegeGranted() method in ACLManager, then you can ignore those rules. Most of the time, you just need to be sure the data the method relies on is already in the database. Of course, you should have the proper way to verify the result as well.

Conclusion

We have looked for a better way to test our EJB and web applications for a long time. We have seen many ideas and debates on this topic, such as how many databases are required, which framework is better, etc. Instead of finding a perfect solution, we tried to simplify the problem. The simplest solution could possibly work.

Resources

Copyright © 2009 O'Reilly Media, Inc.