Wednesday, September 9, 2009

Spring Test Framework Cheatsheet

This documents some prototyping done with the Spring Test Framework (I'll refer to this as the STF). Effectively it is a paraphrase/summary of what's written up in the Chapter 10 of the Spring Framework Reference document, version 3.0.M3, with some example code snippets that I was able to get working. Consult that writeup for further detail.

Here are the high-level features of the STF that I'd consider most interesting:
  1. How to fetch the Spring application context object regardless of environment (web container, test environment, etc.)
  2. How to configure test-specific Spring contexts
  3. Simplified access to context beans
  4. Automatic rollback of test case transactions
  5. Auto-rollback without extending a superclass
  6. Odds-n-ends

How to fetch the Spring application context

This first tip is not directly related to the STF, but was discovered during my experiments with it, so I make note of it here.

 In a previous post, I presented a pattern for determining at runtime how to get a reference to the application context that would first look for a web context, and if that failed simply produce a classpath-based context. This pattern requires explicit knowledge not only of what environment is in play, but also knowledge of exactly what Spring context is appropriate for that environment. A simpler pattern can be used as follows - first, provide a class that implements ApplicationAwareContext:


public class ApplicationContextProvider 
    implements ApplicationContextAware { 
    private static ApplicationContext context;
    public static ApplicationContext getApplicationContext() {
        return context;
    }
    public void setApplicationContext(ApplicationContext context) {
        this.context = context;
    }
}

Declare that class as managed by Spring in the context file:

<bean id="appCtxProvider" 
    class="com.twc.registry.common.ApplicationContextProvider">
</bean>

Now, any class that wants a reference to the context instance can do so at runtime, whether in a web or a test/standalone environment:

ApplicationContext context = 
    ApplicationContextProvider.getApplicationContext();
MyBean myBean = (MyBean)context.getBean("my-bean");

That code snippet can of course be encapsulated and parameterized as needed, to facilitate keeping third-party framework specifics in a single point of change.

This precludes the need for a ServletContextListener as described in my previous post, simplifies the mechanics around fetching beans in the context, and eliminates the need for application awareness of explicit subclasses of the Spring ApplicationContext.

In what follows, I'll present an even simpler method for injecting dependencies directly into test classes, such that a reference to the application context is not needed.


How to configure test-specific Spring contexts

If you need to specify certain test fixtures or other test-specific dependencies, it'd be useful to both have these injected (to facilitate reuse and simplify test setup) but keep the configuration for this injection separate from production classes. Using the STF, a convenience idiom supports doing this.

First, to use the STF, add org.springframework.test-3.0.0.M3.jar to your classpath.

Configure a test-specific Spring context using the following annotation:

package com.mybiz;
@ContextConfiguration
public class MyTest {
   @Test
    public void testThis() {
          ApplicationContext ctx = 
              ApplicationContextProvider.getApplicationContext();
          MyBean myBean = (MyBean ) context.getBean("my-bean");
          // now do some tests with myBean...
    }
}

This presumes the existence of a Spring configuration file named com/mybiz/MyTest-context.xml. To otherwise specify the location:

@ContextConfiguration("/WEB-INF/MyTestConfig.xml")

Multiple resources can be specified using comma-separated lists. The default is for subclasses to inherit superclass' resource lists and optionally override bean entries; this inheritance behavior can also be disabled.


Simplified access to context beans

Now, instead of using the ApplicationContextProvider manually as in the above snippet, we can simply do this:

@Autowired  // or can annotate a setter instead of a field
private MyBean myBean;

If there are  multiple MyBean's defined in the Spring context, this type-based wiring will not suffice (without further qualification, which we ignore for now). Instead use injection by name:

@Resource
private MyBean myBean;

However, it appears (empirically...) that @Resource also wires by type, since in the above working example, I hadn't configured a bean with an ID of "myBean". It also "just works" this way:

private MyBean myBean;
@Resource  // also works with @Autowired!
private void setTheBean(MyBean theBean) {  
// notice the field and/or method are both private - but injection still works!
    myBean = theBean;
}


Automatic rollback of test case transactions

By far the most valuable feature of the STF is the ability to execute integration tests against a database without changing the database state - i.e., all transactions are automatically rolled back. Making this work is reasonably simple - first, we'll need Spring context entries to specify a data source and a transaction manager:

<bean id="dbcp-dataSource" 
    class="org.apache.commons.dbcp.BasicDataSource" 
    destroy-method="close">
    <property name="driverClassName" value="${database.driver}"/>
    <property name="url" value="${database.url}"/>
    <property name="username" value="${database.user}"/>
    <property name="password" value="${database.password}"/>
</bean>
<bean id="txnMgr" 
    class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dbcp-dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="txnMgr"/>
<aop:aspectj-autoproxy/>

This context configuration file must be loaded using @ContextConfiguration; we must declare @Transactional either at the class or method level; and since our transaction manager is not named "transactionManager" (the default) -- or if we would ever wish to disable auto-rollback at the class level -- we'll need to add a @TransactionConfiguration annotation:

@ContextConfiguration(locations = {"/WEB-INF/MyTestConfig.xml"})
@Transactional
@TransactionConfiguration(transactionManager="txnMgr", defaultRollback=true)
public class MyDaoTest extends AbstractTransactionalTestNGSpringContextTests {
.......
// override auto-rollback on per-method basis
    @Test
    @Rollback(false)
    public void keepThisTransactionInPlace() {
        ......
    }
    @BeforeTransaction  
// likewise, @AfterTransaction follows the transaction
    public void doBeforeTxn()
    {
// do whatever setup needed, if any, before the transaction executes
    }
}

To gain numerous conveniences, we've extended a Spring class specific to the TestNG framework (also available are Junit-specific superclasses). We could have instead added a handful of cruft to get around doing this extension, if e.g. you need to extend your own test superclass or for some other reason you're adverse to this. But the added cruft is so noisy that you'll be motivated to simply encapsulate it in your own superclass anyway, or if that's not feasible then reuse becomes a copy-paste headache. If possible, simply extending the Spring-provided convenience classes is probably the simplest thing to do.


Auto-rollback without extending a superclass

But if you must know, here's the cruft you'll need to avoid extending AbstractTransactionalTestNGSpringContextTests:

@ContextConfiguration(locations = {"/WEB-INF/MyTestConfig.xml"})
@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, 
    DirtiesContextTestExecutionListener.class,
    TransactionalTestExecutionListener.class})
@Transactional
@TransactionConfiguration(transactionManager="txnMgr")
public class MyDaoTest implements IHookable, ApplicationContextAware {
    private ApplicationContext applicationContext;
    private final TestContextManager testContextManager;
    private Throwable testException;
    public MyDaoTest() {
        this.testContextManager = new TestContextManager(getClass());
    }
    public final void setApplicationContext(
        ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }
    @BeforeClass(alwaysRun = true)
    protected void springTestContextPrepareTestInstance() 
    throws Exception {
        this.testContextManager.prepareTestInstance(this);
    }
    @BeforeMethod(alwaysRun = true)
    protected void springTestContextBeforeTestMethod(Method testMethod) 
    throws Exception {
        this.testContextManager.beforeTestMethod(this, testMethod);
    }
    public void run(IHookCallBack callBack, ITestResult testResult) {
        callBack.runTestMethod(testResult);
        this.testException = testResult.getThrowable();
    }

    @AfterMethod(alwaysRun = true)
    protected void springTestContextAfterTestMethod(Method testMethod) 
    throws Exception {
        try {
            this.testContextManager.afterTestMethod(
                this, testMethod, this.testException);
        }
        finally {
            this.testException = null;
        }
    }

Ugly, ugly, ugly. Encapsulate!


Odds-n-Ends

There are numerous other facilities in the STF; consult Chapter 10 for the full discussion. Worth mentioning here are the following:
  1. automatic caching of loaded application contexts, to save startup time on per-method basis, once for each test fixture. Optionally instruct the framework to reload the context in case changes to state of beans in that context that are not meant to be preserved across all tests in a given fixture.
  2. JDBC utility class to facilitate querying database state.
  3. Mock objects ready-to-go for JNDI and Servlet API.
Overall, two thumbs up: this is definitely a new item in my toolbox.

No comments:

Post a Comment