Chapter 8. Testing

8.1. Introduction

The Spring team (amongst many, many others) considers testing to be an absolutely integral part of enterprise software development.

This chapter is thus concerned with testing. A thorough treatment of testing in the enterprise software space is beyond the scope of this chapter (and indeed this manual); rather, this chapter focuses (briefly) on the value add that the adoption of the IoC principle can bring to unit testing; and (in the main) focuses on the tangible benefits that the Spring Framework provides in the integration testing space.

8.2. Unit testing

One of the main benefits of Dependency Injection is that your code should depend far less on the container than in traditional J2EE development. The POJOs that comprise your application should be testable in JUnit tests, with objects simply instantiated using the new operator, without Spring or any other container. You can use mock objects or many other valuable testing techniques, to test your code in isolation. If you follow the architecture recommendations around Spring you will find that the resulting clean layering and componentization of your codebase will just naturally faciliate easier unit testing. For example, you will be able to test service layer objects by stubbing or mocking DAO interfaces, without any need to access persistent data while running unit tests.

True unit tests typically will run extremely quickly, as there is no runtime infrastructure to set up, whether application server, database, ORM tool etc. Thus emphasizing true unit tests will boost your productivity. The upshot of this is that you don't need this section of the testing chapter to help you write effective unit tests for your IoC-based applications.

8.3. Integration testing

However, it is also important to be able to perform some integration testing without deployment to your application server or actually connecting to enterprise integration systems. This will enable you to test things such as:

  • The correct wiring of your Spring IoC container contexts.

  • Data access using JDBC or ORM tool. This would include thiings such as the correctness of SQL statements / or Hibernate XML mapping file configuration.

The Spring Framework provides first class support for integration testing. This support is captured in a number of classes in the spring-mock.jar file that ships with the Spring Framework distribution. The classes in this library can be thought of as a significantly superior alternative to in-container testing using tools such as Cactus.

[Note]Note

The test classes described in the rest of this chapter are JUnit-specific.

The org.springframework.test package provides valuable superclasses for integration testing using a Spring container, while at the same time not being reliant on an application server or other deployed environment. Such tests can run in JUnit - even in an IDE - without any special deployment step. They will be slower to run than unit tests, but much faster to run than Cactus tests or remote tests relying on deployment to an application server.

The various abstract classes in this package provide the following functionality:

  • Spring IoC container caching between test case execution.

  • The Dependency Injection of the test fixtures themselves.

  • Transaction management appropriate to integration testing.

  • Inherited instance variables useful for testing.

Numerous Interface21 and other projects since late 2004 have demonstrated the power and utility of this approach. Let's look at some of the important areas of functionality in detail.

8.3.1. Context management and caching

The org.springframework.test package provides support for consistent loading of Spring contexts, and caching of loaded contexts. The latter is important, because if you are working on a large project, startup time may become an issue - not because of the overhead of Spring itself, but because the objects instantiated by the Spring container will themselves take time to instantiate. For example, a project with 50-100 Hibernate mapping files might take 10-20 seconds to load the mapping files, and incurring that cost before running every single test case in every single test fixture leads to slower overall test runs that could (and probably will) reduce productivity.

To address this issue, the AbstractDependencyInjectionSpringContextTests has an abstract protected method that subclasses must implement, to provide the location of contexts:

protected abstract String[] getConfigLocations();

Implementations of this method must provide an array containing the resource locations of XML configuration metadata - typically on the classpath - used to configure the application. This will be the same, or nearly the same, as the list of configuration locations specified in web.xml or other deployment configuration.

By default, once loaded, the set of configuration will be reused for each test case. Thus the setup cost will be incurred only once, and subsequent test execution will be much faster.

In the unlikely case that a test may 'dirty' the config location, requiring reloading - for example, by changing a bean definition or the state of an application object - you can call the setDirty() method on AbstractDependencyInjectionSpringContextTests to cause it to reload the configurations and rebuild the application context before executing the next test case.

8.3.2. Dependency Injection of test fixtures

When AbstractDependencyInjectionSpringContextTests (and subclasses) load your application context, they can optionally configure instances of your test classes by Setter Injection. All you need to do is to define instance variables and the corresponding setters. AbstractDependencyInjectionSpringContextTests will automatically locate the corresponding object in the set of configuration files specified in the getConfigLocations() method.

Let's look at a simple example of this quite powerful feature in action. Consider the scenario where we have a class, HibernateTitleDao, that performs data access logic for say, the Title domain object. We want to write integration tests that test all of the following areas:

  • The Spring configuration; i.e. is everything related to the HibernateTitleDao correct and present?

  • The Hibernate mapping file configuration; i.e. is everything mapped correctly and are the correct lazy-loading semantics in place?

  • The logic of the HibernateTitleDao; i.e. does this class perform as anticipated?

Let's look at the test class itself (we will look at the configuration immediately afterwards).

public class HibernateTitleDaoTests extends AbstractDependencyInjectionSpringContextTests  {

    // this instance will be (automatically) dependency injected    
    private HibernateTitleDao titleDao;

    
    // a setter method to enable DI of the 'titleDao' instance variable
    public void setTitleDao(HibernateTitleDao titleDao) {
        this.titleDao = titleDao;
    }

    
    public void testLoadTitle() throws Exception {
        Title title = this.titleDao.loadTitle(new Long(10));
        assertNotNull(title);
    }

    // specifies the Spring configuration to load for this fixture
    protected String[] getConfigLocations() {
        return new String[] { "classpath:com/foo/daos.xml" };
    }

}

The file referenced by the getConfigLocations() method ('classpath:com/foo/daos.xml') might look like so...

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC  "-//SPRING//DTD BEAN 2.0//EN"
    "http://www.springframework.org/dtd/http://www.springframework.org/dtd/spring-beans-2.0.dtd">
<beans>

    <!-- this bean will be injected into the HibernateTitleDaoTests class -->
    <bean id="titleDao" class="com/foo/dao/hibernate/HibernateTitleDao">
        <property name="sessionFactory" ref="sessionFactory"/>
    </bean>
    
    <bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
        <!-- dependencies elided for clarity -->
    </bean>

</beans>

The AbstractDependencyInjectionSpringContextTests classes uses autowire by type (for the lowdown on autowiring see the section entitled Section 3.3.6, “Autowiring collaborators”). Thus if you have multiple bean definitions of the same type, you cannot rely on this approach for those particular beans. In that case, you can use the inherited applicationContext instance variable, and explicit lookup using getBean().

If you don't want dependency injection applied to your test cases, simply don't declare any setters. Alternatively, you can extend the AbstractSpringContextTests - the root of the class hierarchy in the org.springframework.test package. It merely contains convenience methods to load Spring contexts, and performs no Dependency Injection of the test fixture.

8.3.3. Transaction management

One common issue in tests that access a real database is their effect on the state of the persistence store. Even when you're using a development database, changes to the state may affect future tests. Also, many operations - such as inserting to or modifying persistent data - can't be done (or verified) outside a transaction.

The org.springframework.test.AbstractTransactionalDataSourceSpringContextTests superclass (and subclasses) exist to meet this need. By default, they create and roll back a transaction for each test case. You simply write code that can assume the existence of a transaction. If you call transactionally proxied objects in your tests, they will behave correctly, according to their transactional semantics.

AbstractTransactionalSpringContextTests depends on a PlatformTransactionManager bean being defined in the application context. The name doesn't matter, due to the use of autowire by type.

Typically you will extend the subclass, AbstractTransactionalDataSourceSpringContextTests. This also requires that a DataSource bean definition - again, with any name - be present in the configurations. It creates a JdbcTemplate instance variable that is useful for convenient querying, and provides handy methods to delete the contents of selected tables (remember that the transaction will roll back by default, so this is safe).

If you want a transaction to commit - unusual, but useful if you want a particular test to populate the database, for example - you can call the setComplete() method inherited from AbstractTransactionalSpringContextTests. This will cause the transaction to commit instead of roll back.

There is also convenient ability to end a transaction before the test case ends, through calling the endTransaction() method. This will roll back the transaction by default, and commit it only if setComplete() had previously been called. This functionality is useful if you want to test the behavior of 'disconnected' data objects, such as Hibernate-mapped objects that will be used in a web or remoting tier outside a transaction. Often, lazy loading errors are discovered only through UI testing; if you call endTransaction() you can ensure correct operation of the UI through your JUnit test suite.

These test support classes are designed to work with a single database.

8.3.4. Convenience variables

When you extend the AbstractTransactionalDataSourceSpringContextTests class you will have access to the following protected instance variables:

  • applicationContext (ConfigurableApplicationContext): inherited from the AbstractDependencyInjectionSpringContextTests superclass. Use this to perfom explicit bean lookup, or test the state of the context as a whole.

  • jdbcTemplate: inherited from AbstractTransactionalDataSourceSpringContextTests. Useful for querying to confirm state. For example, you might query before and after testing application code that creates an object and persists it using an ORM tool, to verify that the data appears in the database. (Spring will ensure that the query runs in the scope of the same transaction.) You will need to tell your ORM tool to 'flush' its changes for this to work correctly, for example using the flush() method on Hibernate's Session interface.

Often you will provide an application-wide superclass for integration tests that provides further useful instance variables used in many tests.

8.3.5. Example

The PetClinic sample application included with the Spring distribution illustrates the use of these test superclasses (Spring 1.1.5 and above). Most test functionality is included in the AbstractClinicTests, for which a partial listing is shown below:

public abstract class AbstractClinicTests
               extends AbstractTransactionalDataSourceSpringContextTests {

   protected Clinic clinic;


   public void setClinic(Clinic clinic) {
      this.clinic = clinic;
   }


   public void testGetVets() {
      Collection vets = this.clinic.getVets();
      assertEquals('JDBC query must show the same number of vets',
         jdbcTemplate.queryForInt('SELECT COUNT(0) FROM VETS'), 
         vets.size());
      Vet v1 = (Vet) EntityUtils.getById(vets, Vet.class, 2);
      assertEquals('Leary', v1.getLastName());
      assertEquals(1, v1.getNrOfSpecialties());
      assertEquals('radiology', ((Specialty) v1.getSpecialties().get(0)).getName());
      Vet v2 = (Vet) EntityUtils.getById(vets, Vet.class, 3);
      assertEquals('Douglas', v2.getLastName());
      assertEquals(2, v2.getNrOfSpecialties());
      assertEquals('dentistry', ((Specialty) v2.getSpecialties().get(0)).getName());
      assertEquals('surgery', ((Specialty) v2.getSpecialties().get(1)).getName());
}

Notes:

  • This test case extends the AbstractTransactionalDataSourceSpringContextTests class, from which it inherits Dependency Injection and transactional behavior.

  • The clinic instance variable - the application object being tested - is set by Dependency Injection through the setClinic(..) method.

  • The testGetVets() method illustrates how the inherited JdbcTemplate variable can be used to verify correct behavior of the application code being tested. This allows for stronger tests, and lessens dependency on the exact test data. For example, you can add additional rows in the database without breaking tests.

  • Like many integration tests using a database, most of the tests in AbstractClinicTests depend on a minimum amount of data already in the database before the test cases run. You might, however, choose to populate the database in your test cases also - again, within the one transaction.

The PetClinic application supports four data access technologies - JDBC, Hibernate, TopLink, and JPA. Thus the AbstractClinicTests class does not itself specify the context locations - this is deferred to subclasses, that implement the necessary protected abstract method from AbstractDependencyInjectionSpringContextTests.

For example, the Hibernate implementation of the PetClinic tests contains the following method:

public class HibernateClinicTests extends AbstractClinicTests {

   protected String[] getConfigLocations() {
      return new String[] { 
         '/org/springframework/samples/petclinic/hibernate/applicationContext-hibernate.xml' 
      };
   }
}

As the PetClinic is a very simple application, there is only one Spring configuration file. Of course, more complex applications will typically break their Spring configuration across multiple files.

Instead of being defined in a leaf class, config locations will often be specified in a common base class for all application-specific integration tests. This may also add useful instance variables - populated by Dependency Injection, naturally - such as a HibernateTemplate, in the case of an application using Hibernate.

As far as possible, you should have exactly the same Spring configuration files in your integration tests as in the deployed environment. One likely point of difference concerns database connection pooling and transaction infrastructure. If you are deploying to a full-blown application server, you will probably use its connection pool (available through JNDI) and JTA implementation. Thus in production you will use a JndiObjectFactoryBean for the DataSource, and JtaTransactionManager. JNDI and JTA will not be available in out-of-container integration tests, so you should use a combination like the Commons DBCP BasicDataSource and DataSourceTransactionManager or HibernateTransactionManager for them. You can factor out this variant behavior into a single XML file, having the choice between application server and 'local' configuration separated from all other configuration, which will not vary between the test and production environments.

8.3.6. Running integration tests

Integration tests naturally have more environmental dependencies than plain unit tests. Such integration testing is an additional form of testing, not a substitute for unit testing.

The main dependency will typically be on a development database containing a complete schema used by the application. This may also contain test data, set up by a tool such as a DBUnit, or an import using your database's tool set.

8.4. Further Resources

This section contains links to further resources about testing in general.