Update 2007-05-01: You probably can not follow this article if you did not work with Spring tests yet. The key to this article is at the end, where I show how to call an injected object while it has been proxied by Spring.A colleague (Levi Hoogenberg) showed me a working example of integration tests with Spring. The key is to make a JUnit test that inherits from AbstractAnnotationAwareTransactionalTests (they like long names at Spring). Simply override getConfigLocations() and a complete spring context will be loaded. Any setters in your test class will automatically be called with beans from the context (called auto-wiring). In addition, you can execute some SQL to initialize a database (for example by calling executeSqlScript() in onSetUpBeforeTransaction()). Each test gets a fresh view at the filled database and runs in a fresh new transaction. My main spring configuration (in spring.xml) sets up a context with all the services and its Hibernate backend. The MySQL database connection is set up in a separate file (spring-db.xml). The test however, is not using MySQL but an in-memory H2 database so that it can easily be run from Continuum. This is configured in another file (spring-test.xml). A big advantage of this setup is that it allows you to test most of the real application wiring. Disadvantage is that it uses Hibernate against H2 and not the MySQL target database. I am not too worried about this, I am not using advanced Hibernate features and I do not have to test Hibernate! So I was happily writing tests until I found out that one integration test went much too far. The service I was trying to test (lets call it AbcService) did more then just save something in the database; it also called another service to schedule a task. In a real integration test I would need to assert that the task would have been scheduled. I realized that I was actually misusing the integration test to also do a unit test on AbcService. Instead of writing a proper unit test, I decided to leave it at that. So how was I going to assert that the scheduler service was called? Here are my attempts: Solution 1: Test specific config files
Since you can not override a small part of the configuration, this solution requires you to duplicate a lot of configuration files. Furthermore, you loose the ability to test the actual application configuration files. As soon as I realized this I gave up on the idea. Solution 2: Override the configured service by changing the setter in the test class
The test class (AbcServiceImplTest) has a setter to inject the service under test like so:
public void setAbcService(AbcService abcService) {
this.abcService = abcService;
}
Pretty standard. So my idea was to override the used scheduler service like this:
public void setAbcService(AbcService abcService) {
this.abcService = abcService;
// Override the scheduler service with a mock
SchedulerService mockSchedulerService = ...
((AbcServiceImpl) abcSerive).setSchedulerService(
mockSchedulerService); // Cast fails!
}
Unfortunately, the passed in abcService is not the real thing. It has been proxied by Spring to add transaction support. The proxy that Spring uses (the standard JDK proxy) can only be casted to the implemented interfaces, and not to an actual class.
After a lot of searching and looking with the debugger, I finally found a solution. Be warned: this is a big hack. Do try this at home, but don't complain when it suddenly fails.
public void setAbcService(AbcService abcService) {
this.abcService = abcService;
// Override the scheduler service with a mock
SchedulerService mockSchedulerService = ...
InvocationHandler invocationHandler =
Proxy.getInvocationHandler(abcService);
try {
invocationHandler.invoke(
abcService,
AbcServiceImpl.class.getMethod(
"setSchedulerService",
new Class[] {SchedulerService.class}),
new Object[] {mockSchedulerService});
} catch (Throwable e) {
fail("setSchedulerService failed");
}
}
Incredible, isn't it?
Thank you for your tip. You saved my Fridays afternoon :)
ReplyDeleteIt seems everything works fine (in a normal setter-driver way) until you decide to use AOP. Since then one have to struggle with proxies and invokers.