Strategies for Testing


The topic of testing application code and the application itself, in all its incarnations, is a large one. It can and has filled numerous books on the subject. All we can reasonably discuss in the scope of this chapter are some testing strategies as they relate to Spring itself. The following text assumes you have at least a basic familiarity with JUnit (www.junit.org).

Unit Tests

The most important point that can be made with regards to unit testing classes that are used in a Spring container is that the majority of the time, the Spring container does not have to be used and should not be used for this type of test. One of the main benefits of IoC is that code is unaware of the container, so in terms of a unit test, even if the container is lightweight like Spring, it's usually faster and easier to use Java code to create and wire together the class being tested, along with its dependencies.

Let's review a test from the previous chapter:

 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"     "http://www.springframework.org/dtd/spring-beans.dtd">      <beans>   <bean  >     <property name="weatherDao">       <ref local="weatherDao"/>     </property>   </bean>   <bean  >   </bean> </beans> public class WeatherServiceTest extends TestCase {   public void testSample2() throws Exception {     ApplicationContext ctx = new ClassPathXmlApplicationContext(         "ch03/sample2/applicationContext.xml");     WeatherService ws = (WeatherService) ctx.getBean("weatherService");          Double high = ws.getHistoricalHigh(         new GregorianCalendar(2004, 0, 1).getTime());     // ... do more validation of returned value here...   } } 

In the last chapter, we were demonstrating container functionality, so it was appropriate to have the test do its work through the container. However, if your main interest is in testing WeatherServiceImpl, you are better off doing everything as Java code:

 public class WeatherServiceTest extends TestCase {        public void testWithRealObjects () throws Exception {          WeatherServiceImpl ws = new WeatherServiceImpl();     ws.setWeatherDao(new StaticDataWeatherDaoImpl());          Double high = ws.getHistoricalHigh(         new GregorianCalendar(2004, 0, 1).getTime());     // ... do more validation of returned value here...   } } 

This is an artificially simple test, in that there is only one collaborator for the main class, and even so, we are not actually validating as much as we would like to; we don't know that the weather service is actually calling the weather DAO and using the result returned by the latter. Because we are working against interfaces, why don't we just create a mock object implementing the weather DAO interface, just for the purpose of the test? This will allow us to not worry about test data, as we would when testing with the real StaticDataWeatherDaoImpl:

 public void testWithMock() throws Exception {        WeatherServiceImpl ws = new WeatherServiceImpl();          ws.setWeatherDao(new WeatherDao() {       public WeatherData find(Date date) {            WeatherData wd = new WeatherData();       wd.setDate(date);       wd.setLow(10);       wd.setHigh(20);       return wd;     }          // 2 methods not needed for test     public WeatherData save(Date date) { return null; }     public WeatherData update(Date date) { return null; }     });          Double high = ws.getHistoricalHigh(new GregorianCalendar(2004, 0, 1).getTime());     assertTrue(high.equals(new Double(20))); } 

In this case the mock object was created inline as an anonymous class. Many times, it is useful to define a separate static mock class that can be reused. Additionally, if more than one test is going to need the mock object, it's useful to create it in the JUnit setup() method. Spring actually includes some mock classes, including, among others, MockHttpSession, MockHttpServletRequest, and MockHttpServletResponse, to mock the HttpSession, HttpServletRequest, and HttpServletResponse classes, respectively. These are used for internal Spring unit tests, and you may use them yourself when testing your web layer controllers.

It is sometimes painful to try to mock as a normal class a complicated interface with many operations. EasyMock (www.easymock.org) is an extension library that can dynamically create a mock object for a specified interface (and with a small add-on, also for a specified class). Let's look at a version of the previous test that uses a weather DAO mock generated by EasyMock:

 public void testWithEasyMock() throws Exception {        // create test data   Date date = new GregorianCalendar(2004, 0, 1).getTime();   WeatherData wd = new WeatherData();   wd.setDate(date);   wd.setLow(10);   wd.setHigh(20);        // create mock weather dao with EasyMock, returning test data on find() call   MockControl control = MockControl.createControl(WeatherDao.class);   WeatherDao weatherDao = (WeatherDao) control.getMock();   // set the method we expect called on the mock, and return value   weatherDao.find(date);   control.setReturnValue(wd);        WeatherServiceImpl ws = new WeatherServiceImpl();   ws.setWeatherDao(weatherDao);        // turn on the mock   control.replay();   Double high = ws.getHistoricalHigh(date);   // verify the mock actually received a find() call   control.verify();        assertTrue(high.equals(new Double(20))); } 

In the example, we create some test data, ask EasyMock to create for us a mock object implementing the weather DAO interface, register a method call on the mock object, specify that the mock should return the test data, and then feed the mock to the weather service, which is what we actually test. Afterwards, we can ask EasyMock to verify that the method we registered actually got called. There are a number of optional features, such as the ability to specify the minimum and maximum number of times a method is called, and verifying the order of method calls.

While this sample actually ends up longer than the one using a simple class-based mock, you can see how simple it is to create a mock object for an arbitrary interface, and how this approach can save significant time and effort for some mocking scenarios. When considering the use of EasyMock, you should also take a look at jMock (www.jmock.org), which is another dynamic mock library. We prefer the somewhat different approach used by jMock, for some scenarios.

For each unit test, you should carefully consider which approach to mocking makes the most sense, and in fact whether mocking makes sense at all, as opposed to state-based testing as in our first example, where real objects are used.

Tests That Use the Spring Container

For a relatively small percentage of unit tests, it may make sense to use a Spring container to do object wiring and configuration, as it is the least amount of work.

For most integration tests, on the other hand, it will make sense to use the container. The term integration test is somewhat nebulous, but it generally means a test that tries to use a number of classes or components working together, usually in the same fashion as for normal application usage. Often, especially if the container definition is split up into multiple files, it's possible to use the same or most of the same container definition as is used when the application is run normally. If the Spring-based app needs tobe deployed in an appserver for normal usage, the integration test can run inside or outside of the appserver, but it's preferable if possible to run it outside the appserver because eliminating the deployment step can reduce a build-deploy-test, as an example, from 2 minutes to 2 seconds, increasing productivity greatly. Whether it's possible to run the integration test outside of the appserver really depends on what set of classes or components of the application is being tested, and whether there are dependencies on the appserver that cannot be removed, or would make the test irrelevant if removed. One advantage to running the test inside the appserver is that the execution environment and deployment scenario is more similar to the final execution environment, reducing the potential surprise of having an integration test work, but having the same classes or components fail when used normally in the deployed app. Except for the view layer or EJBs, this is not usually a great concern, but a reasonable approach is to try to do most integration testing outside of the appserver, but also to do some in-appserver testing.

When the test is run outside of the appserver, it may simply be a normal JUnit test. When the test is run inside the appserver, then some sort of mechanism is actually needed to kick it off inside the appserver after the application is deployed. One of the most convenient approaches is something like Jakarta Apache Cactus (http://jakarta.apache.org/cactus/), which allows a command-line ant build to transparently run a normal JUnit test remotely, inside the deployed test application in the appserver. The other approach is to simply do integration testing by driving the view layer itself, with a tool like Canoo WebTest (http://webtest.canoo.com/webtest/manual/WebTestHome.html) to use view layer functionality that will stress the classes that need to be tested. While tests of the view layer itself are very useful, if in-appserver integration testing of lower-layer code is actually needed, an approach similar to that afforded by Cactus is much more convenient, as normal JUnit tests, which directly drive the relevant code, may be used.

Let's focus on using JUnit to drive integration tests.

Managing the Container Configuration in a Test

You've seen a number of JUnit test samples already where the test method itself loads the application context (or bean factory). For a real-life integration test it is generally more appropriate to load the context in the JUnit setUp() method (and closed in the tearDown()), so that each test method does not have to repeat this step. One concern is that while this will avoid code duplication, the context will still be created once for each test method, as calls to setup()/teardown() surround each test method invocation. JUnit does this to ensure fresh base test data, but when using a context that has an initialization operation that is time-intensive, such as creating a Hibernate SessionFactory, this is less than ideal, especially if, as in most cases, the same context instance could be reused for all test methods.

Inside the spring-mock.jar add-on JAR, Spring includes a convenience base test case class, AbstractDependencyInjectionSpringContextTests, which makes it easy to load and reuse just one context instance for all the test methods in a test case. As an additional benefit, the base class is capable of triggering the direct injection of dependencies into the test case class, via setter injection(autowired by type), or field injection matching by name. Let's examine how the test case from the beginning of this section would look, as implemented via the use of AbstractDependencyInjectionSpringContextTests:

 public class WeatherServiceTest extends     AbstractDependencyInjectionSpringContextTests {        private WeatherService weatherService;        public void setWeatherService(WeatherService weatherService) {     this.weatherService = weatherService;   }        protected String[] getConfigLocations() {     return new String[]{"ch03/sample3/applicationContext.xml"};   }        public void testWeatherService() throws Exception {     Double high = weatherService.getHistoricalHigh(         new GregorianCalendar(2004, 0, 1).getTime());     // ... do more validation of returned value here...   } } 

There is no longer any explicit loading of the application context by the test. The template method getConfigLocations() is implemented to simply return one or more location strings for XML fragments that make up the context definition. The WeatherService instance is now directly injected into the test as a property, instead of the test having to ask for it. After the initial load, the context is actually cached based on a key, which is normally the config locations. This means that all test methods, and even other tests that use the same locations, will all share the same context instance. On the other hand, the property (or field) injection will still happen before each test method. If a test knows that it will modify or has modified the context (for example, it has replaced a bean definition), and wants to force the context to be reloaded before the next text method is executed, it may call the setDirty() method.

Another set of base test case classes provided by Spring, AbstractTransactionalSpringContextTests and AbstractTransactionalDataSourceSpringContextTests, can be very useful for testing database-related code. When testing this type of code, including DAOs/Mappers, a common problem or inconvenience is how to handle the fact that the database state is modified by a test of this type. Typically, this is handled by one of several approaches. Tests can try to ensure that only data that doesn't interfere with other data is produced or modified. Alternately, the program driving the test (such as an ant build script) can initialize the database to a known state before and/or after the test. Alternately, the test itself can try to take care of database initialization or cleanup.

The approach offered by these base test case classes is, using Spring's transaction abstraction (which will be fully described in subsequent chapters), to automatically create (in the JUnit setUp() method) a database transaction before the test is executed, and when the test is done, to automatically roll back the transaction (in the teardown() method). The test is free to insert or delete any data in the database as needed, without affecting other tests, as the state will be reset afterwards by the rollback. If there is a need to leave the data as-is after a test, this can be done by calling a setComplete() method. This approach is quite usable for a number of test scenarios, with only a few caveats. One is that the test itself must make sure to flush data as appropriate for the particular data access technology. Spring's Hibernate integration support, for example, ensures that all data in the Hibernate session is flushed (if needed) when a transaction commits. Because a test using these base classes will never end up committing the transaction, it's important for the test to manually, as a last step, tell Hibernate to flush all data out to the database so that any problems such as database constraint violations are still caught.

In-Appserver Tests

Chapter 11, "Spring and EJB," demonstrates how to use Apache Cactus to execute test code inside an application deployed in an appserver. While it does this in order to test EJBs, this technique is valid for testing any code inside an appserver, so we direct you to that chapter to learn how to use Cactus in this fashion.



Professional Java Development with the Spring Framework
Professional Java Development with the Spring Framework
ISBN: 0764574833
EAN: 2147483647
Year: 2003
Pages: 188

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net