6.8 Testing Server-Side Business Logic

6.8.1 Problem

You want to test business logic that normally depends on a database, but mocking out the low-level SQL is far too complex.

6.8.2 Solution

Organize your server-side code using business objects and database access objects (DAOs). Place all business logic in your business objects, and all database access in your DAOs. Use a factory to create mock implementations of your DAOs when testing your business objects.

6.8.3 Discussion

We showed how to write mock objects to simulate low-level SQL code earlier in this chapter. It is a useful technique for testing the data access tier of your application, but tends to be far too complex for business logic tests. For business objects, you should strive to create mock implementations of the entire data access tier, rather than mock implementations of the JDBC interfaces.

Figure 6-1 illustrates a common design pattern for server-side Java code. In this diagram, either an EJB or a servlet dispatches method calls to CustomerBO, a business object that contains server-side business logic. The business object is what we would like to test.

Figure 6-1. Business object and DAO pattern
figs/jexp_0601.gif

The first box in Figure 6-1 shows either an EJB or a servlet. This pattern works well with either approach, although the EJB approach allows you to easily invoke many different business objects under the umbrella of a single transaction. Regarding testing, the business object pattern is fantastic because you can test CustomerBO as you would test any other standalone Java class. That is, you don't need to run your tests inside of the application server.

The second key to making business objects testable is keeping data access code separate. The CustomerDAO interface defines an API to a data source, and the OracleCustomerDAO is an Oracle-specific implementation. When using this approach, your business objects generally locate the correct DAO implementations using some sort of factory object. Example 6-15 shows what some of the methods in CustomerDAO might look like.

Example 6-15. CustomerDAO methods
public interface CustomerDAO {
    Customer getCustomer(long customerId) throws DataSourceException;
    void deleteCustomer(long customerId) throws DataSourceException;
    CustomerSummaryInfo[] searchByFirstName(String firstName)
            throws DataSourceException;
}

There are no specific requirements for the DAO, other than that it should not expose JDBC implementation details to the caller. Notice that our methods all throw DataSourceException, which is an exception we made up for this example. If our methods throw SQLException, it would make them harder to implement for non-relational data sources.

Rather than creating a mock DAO implementation, you might want to create a DAO implementation that hits a small, local database rather than the official database. This allows you to run tests against small, easily configured data without the political battles often required to make changes to the main project database.

Example 6-16 shows an imaginary test case for the business object.

Example 6-16. Imaginary test case for CustomerBO
public class TestCustomerBO extends TestCase {
    public void testSomething(  ) throws DataSourceException {

        // instantiate and test the business object
        CustomerBO custBo = new CustomerBO(  );
        assertEquals("...", custBo.doSomething(  ));
    }
}

The test constructs a CustomerBO and calls methods on it. It is within these methods that the CustomerBO presumably performs the business logic that we are testing. Example 6-17 shows what a method in CustomerBO might look like.

Example 6-17. CustomerBO method
public class CustomerBO {
    public void deleteCustomer(long customerId) 
            throws DataSourceException {
        CustomerDAO dao = MyDAOFactory.getCustomerDAO(  );
        dao.deleteCustomer(customerId);
        ...perhaps some business logic here
    }
}

From the perspective of CustomerBO, the actual DAO implementation is completely unknown. The MyDAOFactory class takes care of instantiating the correct DAO, whether it is a mock implementation or the real Oracle implementation. You will have to come up with a mechanism to inform the factory which DAO implementation to create. An easy approach is to set a system property in your Ant buildfile. The system property allows you to avoid hardcoding in your application, making it possible to plug in different DAO implementations in the future.

The details of the mock DAO implementations are not important. The general rule is that they should do as little as possible. Their sole purpose is to support the unit tests, so they should be implemented on an as-needed basis to support different tests. They are nothing more than hardcoded dummy classes.

6.8.4 See Also

Search for "J2EE Patterns Catalog" on Google. It should bring up links to Sun's Java Blueprints documentation, which explains the DAO pattern in detail. Our implementation assumes that the business object is a standalone Java class, while Sun's examples usually implement the business object as an EJB. This topic is also discussed in Chapter 11.