Spring 2.5 ships with great support for integration testing through the classes in the org.springframework.test package. These classes allow you to dependency inject your test cases off of your existing Spring configuration, either by using your production Spring configuration file or one you've defined especially for the test case. This post explains how to annotate your JUnit 4 test cases to be autowired, but there's a lot more too the new spring-test.jar and it works with JUnit 3.8 also.
public class MyService {
private final String name;
public MyService(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
This service is defined in a Spring XML configuration file as: <bean id="myService" class="org.sample.MyService" >
<constructor-arg value="simple service" />
</bean>
To have Spring dependency inject our test case, we're going to have to annotate it with a little information. We'll use the standard JUnit @RunWith annotation to tell JUnit to use the Spring TestRunner, the Spring @ContextConfiguration annotation to indicate the TestCase needs injection, and the Spring @Autowired annotation to inject the MyService field by type.@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public final class SimpleAutowireTest {
@Autowired
private MyService service;
@Test
public void testServiceName() {
assertEquals("simple service", service.getName());
}
}
There are a variety of ways to have Spring run your test. Besides SpringJUnit4ClassRunner.class there are also abstract TestCase objects you can inherit from, including some that will expose the ApplicationContext to you. By default, the spring configuration file for the test case will be [TestName]-context.xml. You can easily override this to point the test at your production configuration file. Also, the @Autowired annotation autowires the field by type. In the case of conflicts, you can specify the bean name using the @Qualifier annotation. Consider the following Spring configuration where there are two beans of type MyService in a file named applicationContext.xml: <bean id="deliveryService" class="org.sample.MyService" >
<constructor-arg value="delivery service" />
</bean>
<bean id="accountingService" class="org.sample.MyService" >
<constructor-arg value="accounting service" />
</bean>
The JUnit test now requires the @Qualifier field annotation and a configuration file location as an @ContextConfiguration parameter:@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"applicationContext.xml"})
public final class AdvancedAutowireTest {
@Autowired
@Qualifier("deliveryService")
private MyService service;
@Test
public void testServiceName() {
assertEquals("delivery service", service.getName());
}
}
To note: if you point the test to the production Spring configuration, then you're running integration tests and not unit tests. Integration tests are valuable and necessary, but are not always a wholesale replacement of unit tests. There's still value in isolation testing each of the components with mock dependencies. What I like about using Spring for the integration tests is that it validates the Spring mapping at test time. On my projects, we occasionally have the unit tests all passing but the Spring configuration contains a circular dependency, and we don't find out until the build hits QA. A Spring integration test pushes up error time to the test phase, when a developer can take quick action and long before QA is held up.As is standard with JUnit, each test method runs within its own instance of the TestCase. However, the Spring TestRunner will only create one ApplicationContext for all the instances of the test method. This means that if you're test method destroys the state of a singleton bean, then downstream tests methods using that bean will fail. The way to work around this is to annotate your method with the @DirtiesContext annotation. This tells the Spring TestRunner to reload the context between test methods. For instance, these tests will pass when annotated, but fail when not:
@Autowired
private MyConfigurableService service;
@Test
@DirtiesContext
public void testServiceName1() {
assertEquals("configurable service", service.getName());
service.setName("Updating Name");
}
@Test
@DirtiesContext
public void testServiceName2() {
assertEquals("configurable service", service.getName());
service.setName("Updating Name");
}
There's a lot more in spring-test, but this is a good starting point. The transaction annotations look especially promising, including the ability to open and rollback transactions per method. The only reference I know for spring-test is the Javadoc at http://static.springframework.org/spring/docs/2.5.x/api/org/springframework/test/package-summary.html. Those looking for a Spring starter lesson might consult the DZone RefCard.The dependencies for spring-test are: junit, spring-context, spring-core, spring-beans, spring-tx, and commons-logging. A sample Gradle script for your build is at href="http://svn.assembla.com/svn/SampleCode/spring-test/build.gradle.
All the code for this post (including Gradle build script and IDEA project) is posted at http://svn.assembla.com/svn/SampleCode/spring-test/ and can be downloaded through subversion with the following command:
svn co http://svn.assembla.com/svn/SampleCode/spring-test
3 comments:
Good post, Hamlet! One of the nicer features of the TestContext framework is its extensibility. All of its core functionality is delivered via implementations of TestExecutionListener. You can create custom listeners to add new functionality or incorporate the functionality of other tools.
I have an example of integrating DBUnit on Google Code you may be interested in:
http://code.google.com/p/springdbunit/
The book "Pro Spring 2.5" has some information on these features and some examples.
Thanks so much!
Information about @Qualifier tag was really useful!
Post a Comment