Saturday, December 29, 2012

Mockito and Spring proxies

I use Spring and I use Mockito. Mockito is extremely good at keeping unit tests fast but of course it is also beneficial to sometimes test by wiring up Spring (integration tests). However, when I do integration tests I sometimes only want to wire up part of the world and mock out other parts. Let me introduce some code before continuing. Say we have this code:

@Component
public class PostDistrictFinder {
 
 public String getDistrict(String letter) {

  // this will involve some complex lookup into an even more complex Cobol
  // based system

  return "SOME DISTRICT";

 }

}



@Component
public class PostCentral {

 @Autowired
 private PostDistrictFinder finder;


 //This is just a plain annotation to force Spring to make a proxy
 @Timed
 public void processLetter(String letter) {

  // process letter, e.g. calculate whether postage is correct or send out
  // invoice, validate address as known etc.

  String district = finder.getDistrict(letter);
  System.err.println(district);

  // forward letter to district

 }

}


public class Main {

 public static void main(final String[] args) {
  final ApplicationContext context = new ClassPathXmlApplicationContext("META-INF/spring/mock-context.xml");

  final PostCentral bean = context.getBean(PostCentral.class);

  bean.processLetter("");
 }
}
Essentially it is just one bean using another bean. Running this code will print "SOME DISTRICT".

Say I have this test, what do you think it prints?:
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public class MockServiceTest {


 @Mock
 private PostDistrictFinder pdf;
 
 
 @Autowired
 @InjectMocks
 private PostCentral poc;

 
 @Before
 public void setup(){
  MockitoAnnotations.initMocks(this);
  Mockito.when(pdf.getDistrict(Mockito.anyString())).thenReturn("Mocked value");
 }
 
 
 @Test
 public void test() throws Exception {

  poc.processLetter("");
  
 }

}
Does it print "SOME DISTRICT" or "Mocked value"? What do you think it prints if you remove the @Timed annotation on the processLetter in the PostCentral class?

Well the above method prints "SOME DISTRICT" and not "Mocked value" with the @Timed annotation on the method and "Mocked value" without the @Timed annotation (note that the result would have been the same had I used @Transactional).

Why is that? The reason is that Spring will create a proxy of the PostCentral class if it is annotated with @Timed (or @Transaction) or any other annotation that causes a proxy to be created. Set a break point in the code and verify if you are in doubt.

How do you get the test to work? You might think that this is enough:
 @Before
 public void setup(){
  MockitoAnnotations.initMocks(this);
  ReflectionTestUtils.setField(poc, "finder", pdf);
  Mockito.when(pdf.getDistrict(Mockito.anyString())).thenReturn("Mocked value");
 
 }
However, this is not the case, as you are operating on the proxy and not the proxied object. You can however do this:


 //REMOVE THE @InjectMocks FROM THE PostCentral (poc) FIELD

 @Before
 public void setup() throws Exception{
  MockitoAnnotations.initMocks(this);
  PostCentral pc = (PostCentral) unwrapProxy(poc);
  ReflectionTestUtils.setField(pc, "finder", pdf);
  Mockito.when(pdf.getDistrict(Mockito.anyString())).thenReturn("Mocked value");
 
 }

 //http://forum.springsource.org/showthread.php?60216-Need-to-unwrap-a-proxy-to-get-the-object-being-proxied
 
 public static final Object unwrapProxy(Object bean) throws Exception {
  
  /*
   * If the given object is a proxy, set the return value as the object
   * being proxied, otherwise return the given object.
   */
  if (AopUtils.isAopProxy(bean) && bean instanceof Advised) {
   
   Advised advised = (Advised) bean;
   
   bean = advised.getTargetSource().getTarget();
  }
  
  return bean;
 }


If the above makes sense, then fine. If you want to understand the inner workings of Spring AOP/proxies, then read this http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/aop.html#aop-understanding-aop-proxies.