Chapter 23. Testing

23.1. Unit testing

You don't need this manual to help you write effective unit tests for Spring-based applications.

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--for example, those in J2EE without EJB--you will find that the resulting clean layering will also greatly facilitate 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 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.

23.2. Integration testing

However, it's also important to be able to perform some integration testing without deployment to your application server. This will test things such as:

  • Correct wiring of your Spring contexts.

  • Data access using JDBC or ORM tool--correctness of SQL statements. For example, you can test your DAO implementation classes.

Thus Spring provides valuable support for integration testing, in the spring-mock.jar. This can be thought of as a significantly superior alternative to in-container testing using tools such as Cactus.

The org.springframework.test package provides valuable superclasses for integration tests using a Spring container, but not dependent 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 superclasses in this package provide the following functionality:

  • Context caching.

  • Dependency Injection for test classes.

  • Transaction management appropriate to tests.

  • 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.

23.2.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 them, and incurring that cost before running every test case will greatly reduce productivity.

Thus, AbstractDependencyInjectionSpringContextTests has an abstract protected method that subclasses must implement, to provide the location of contexts:

protected abstract String[] getConfigLocations();

This should provide a list of the context locations--typically on the classpath--used to configure the application. This will be the same, or nearly the same, as the list of config locations specified in web.xml or other deployment configuration.

By default, once loaded, the set of configs 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.

23.2.2. Dependency Injection of test class instances

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.

The superclasses use autowire by type. 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 Setter Injection applied to your test cases, don't declare any setters. Or extend 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.

23.2.3. Transaction management

One common problem 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 persistence 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 a DataSource bean definition--again, with any name--is 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 behaviour 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.

Note that these test support classes are designed to work with a single database.

23.2.4. Convenience variables

When you extend org.springframework.test package you will have access to the following protected instance variables:

  • applicationContext (ConfigurableApplicationContext): inherited from AbstractDependencyInjectionSpringContextTests. 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.

23.2.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 AbstractClinicTests, for which a partial listing is shown belong:

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 org.springframework.AbstractTransactionalDataSourceSpringContextTests, from which it inherits Dependency Injection and transactional behaviour.

  • 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 behaviour 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 three data access technologies--JDBC, Hibernate and Apache OJB. Thus AbstractClinicTests does not specify the context locations--this is deferred to subclasses, that implement the necessary protected abstract method from AbstractDependencyInjectionSpringContextTests.

For example, the JDBC 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 behaviour 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.

23.2.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 a tool such as a DBUnit, or an import using your database's tool set.