13

I have a drools rule file which uses service classes in the rules. So one rule does something like this:

eval(countryService.getCountryById(1) != null)

In a validationservice that is annotated with @service and @Transactional(propagation=Propagation.SUPPORTS) the drools file is used in a statelessKnowledgebase and facts are added that should be used in the drool. Once that is done the session.execute(facts) is invoked and the rule engine starts.

In order to test the rules I would like to stub the countryService.getCountryById(). No big problem using mockito. Done this for other service that use a drools setup as well and it worked fine. However in this particular case the countryService was not stubbed and I couldn't figure out why. After spending a lot of time and checking my code I found that having @Transactional above the service or lacking this annotation made the difference. Lacking the @Transaction made mockito mock the countryservice without any problem, having the @transactional in place caused mockito to fail (without any error or hint) injecting the mock so the original countryservice object was used.

My question is why this annotation causes this problem. Why can't mockito inject the mocks when @Transactional is set? I've noticed that mockito is failing as when I debug and inspect the countryService when it is being added as global to the drools session I see the following difference when I inspect the countryservice in my debugwindow:

  • with @transactional: countryService has the value CountryService$$EnhancerByCGLIB$$b80dbb7b

  • without @transactional:countryService has the value CountryService$$EnhancerByMockitoWithCGLIB$$27f34dc1

In addition with @transactional my breakpoint in the countryservice methode getCountryById is found and the debugger stops at that breakpoint, but without the @transactional my breakpoint is skipped as mockito bypasses it.

ValidationService:

@Service
@Transactional(propagation=Propagation.SUPPORTS)
public class ValidationService 
{
  @Autowired
  private CountryService countryService;

  public void validateFields(Collection<Object> facts)
  {
    KnowledgeBase knowledgeBase = (KnowledgeBase)AppContext.getApplicationContext().getBean(knowledgeBaseName); 
    StatelessKnowledgeSession session = knowledgeBase.newStatelessKnowledgeSession();
    session.setGlobal("countryService", countryService);
    session.execute(facts);

  }

And the test class:

public class TestForeignAddressPostalCode extends BaseTestDomainIntegration
{

  private final Collection<Object> postalCodeMinLength0 = new ArrayList<Object>();

  @Mock
  protected CountryService countryService;

  @InjectMocks
  private ValidationService level2ValidationService;


  @BeforeMethod(alwaysRun=true)
  protected void setup()
  {
    // Get the object under test (here the determination engine)
    level2ValidationService = (ValidationService) getAppContext().getBean("validationService");
    // and replace the services as documented above.
    MockitoAnnotations.initMocks(this);

    ForeignAddress foreignAddress = new ForeignAddress();
    foreignAddress.setCountryCode("7029");
    foreignAddress.setForeignPostalCode("foreign");

    // mock country to be able to return a fixed id
    Country country = mock(Country.class);
    foreignAddress.setLand(country);
    doReturn(Integer.valueOf(1)).when(country).getId();

    doReturn(country).when(countryService).getCountryById(anyInt());

    ContextualAddressBean context = new ContextualAddressBean(foreignAddress, "", AddressContext.CORRESPONDENCE_ADDRESS);
    postalCodeMinLength0.add(context);
  }

  @Test
  public void PostalCodeMinLength0_ExpectError()
  {
    // Execute
    level2ValidationService.validateFields(postalCodeMinLength0, null);

  }

Any idea what to do if I want to keep this @transactional annotation but also be able to stub the countryservice methodes?

regards,

Michael

Michael
  • 233
  • 2
  • 4
  • 11
  • Could you be more precise on how you know why mockito is failing ? Also while not related to the issue, you should note that mocking value is not really recommended, you should instead create a value instance yourself, maybe with a custom factory in your test or a private constructor, etc... – bric3 Oct 12 '12 at 12:23
  • Also could you show a bit more of `BaseTestDomainIntegration` and maybe the spring config if that's relevant. – bric3 Oct 12 '12 at 12:28
  • hi brice, i've added more info. see the bullets – Michael Oct 12 '12 at 12:39
  • the basetestdomainintegration is where I setup my spring testcontext. It is annotated with @TransactionConfiguration(defaultRollback = true) @ContextConfiguration(locations = { "classpath:domain-TestContext.xml" }) – Michael Oct 12 '12 at 12:40
  • In your context do you have a `CountryService` bean ? **If so you might want to create the mock in the spring context instead.** I believe the combination Spring/some aspect/testng base class might trigger some additional behavior after the mock has been created which in your case I suspect is the replacement of the mockito mock. – bric3 Oct 12 '12 at 12:58
  • The validationService has a private member countryService with @Autowired and the spring context uses the and config. So I don't create a countryService myself, but I do annotate the countryservice with at Autowired and mock the countryservice in my testclass as you can see, which is injected through at injectMocks – Michael Oct 12 '12 at 13:08
  • Sorry I wanted to say the opposite in ;y previous comment, i.e you might want to create the `CountryService` mock in the spring context _if it's not already there_ (which is the case as you just said). Another option might be to execute the code in your before method in the test method itself. Also note that `@InjectMocks` which is a mockito annotation doesn't even care about Spring's `@Autowired` annotation, it looks for the field type and name only. – bric3 Oct 12 '12 at 13:24
  • moving the code to the test method doesn't make a difference. I'm trying to get my head around what at transactional could have to do with this problem... – Michael Oct 12 '12 at 14:08

5 Answers5

10

Please note that since Spring 4.3.1, ReflectionTestUtils should automatically unwrap proxies. So

ReflectionTestUtils.setField(validationService, "countryService", countryService);

should now work even if your countryService is annotated with @Transactional, @Cacheable... (that is, hidden behind a proxy at runtime)

Related issue: SPR-14050

darrachequesne
  • 742
  • 7
  • 18
9

Whats happening is your ValidationService is being wrapped in a JdkDynamicAopProxy, so when Mockito goes to inject the mocks into the service it does not see any fields to inject them into. You'll need to do one of two things:

  • Forego starting your Spring Application Context and test just the Validation Service, forcing you to mock every dependency.
  • Or unwrap your implementation from the JdkDynamicAopProxy, and handle injecting the mocks yourself.

Code Example:

@Before
public void setup() throws Exception {
    MockitoAnnotations.initMocks(this);
    ValidationService validationService = (ValidationService) unwrapProxy(level2ValidationService);
    ReflectionTestUtils.setField(validationService, "countryService", countryService);
}

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;
}
SuperSaiyen
  • 1,410
  • 10
  • 12
6

Based on the answer of SuperSaiyen, I created an drop-in utility class to make it simpler & type safe:

import org.mockito.Mockito;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.test.util.ReflectionTestUtils;

@SuppressWarnings("unchecked")
public class SpringBeanMockUtil {
  /**
   * If the given object is a proxy, set the return value as the object being proxied, otherwise return the given
   * object.
   */
  private static <T> T unwrapProxy(T bean) {
    try {
      if (AopUtils.isAopProxy(bean) && bean instanceof Advised) {
        Advised advised = (Advised) bean;
        bean = (T) advised.getTargetSource().getTarget();
      }
      return bean;
    }
    catch (Exception e) {
      throw new RuntimeException("Could not unwrap proxy!", e);
    }
  }

  public static <T> T mockFieldOnBean(Object beanToInjectMock, Class<T> classToMock) {
    T mocked = Mockito.mock(classToMock);
    ReflectionTestUtils.setField(unwrapProxy(beanToInjectMock), null, mocked, classToMock);
    return mocked;
  }
}

Usage is simple, just on the beginning of your test method, call the method mockFieldOnBean(Object beanToInjectMock, Class<T> classToMock) with the bean that you want to inject a mock on, and the class of the object that should be mocked. Example:

Let's say you have a bean with type SomeService which holds an autowired bean of SomeOtherService, something like;

@Component
public class SomeService {
  @Autowired
  private SomeOtherService someOtherService;

  // some other stuff
}

To mock someOtherService on the SomeService bean, use the following:

@RunWith(SpringJUnit4ClassRunner.class)
public class TestClass {

  @Autowired
  private SomeService someService;

  @Test
  public void sampleTest() throws Exception {
    SomeOtherService someOtherServiceMock = SpringBeanMockUtil.mockFieldOnBean(someService, SomeOtherService.class);

    doNothing().when(someOtherServiceMock).someMethod();

    // some test method(s)

    verify(someOtherServiceMock).someMethod();
  }
}

everything should work as they should.

Community
  • 1
  • 1
Utku Özdemir
  • 7,390
  • 2
  • 52
  • 49
2

An alternative solution is to add the mock object to the Spring context before Spring wires everything together, so that it will have already been injected before your tests begin. The modified test might look something like this:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { Application.class, MockConfiguration.class })
public class TestForeignAddressPostalCode extends BaseTestDomainIntegration
{

  public static class MockConfiguration {

      @Bean
      @Primary
      public CountryService mockCountryService() {
        return mock(CountryService.class);
      }

  }

  @Autowired
  protected CountryService mockCountryService;

  @Autowired
  private ValidationService level2ValidationService;

  @BeforeMethod(alwaysRun=true)
  protected void setup()
  {

    // set up you mock stubs here
    // ...

The @Primary annotation is important, making sure that your new mock CountryService has top priority for injection, replacing the normal one. This may have unintended side effects, however, if the class is injected in multiple places.

pimlottc
  • 3,066
  • 2
  • 29
  • 24
0

The exist an Spring utility called AopTestUtils in the Spring Test module.

public static <T> T getUltimateTargetObject(Object candidate)

Get the ultimate target object of the supplied candidate object, unwrapping not only a top-level proxy but also any number of nested proxies. If the supplied candidate is a Spring proxy, the ultimate target of all nested proxies will be returned; otherwise, the candidate will be returned as is.

You can inject a mock or spy and unproxy the class during the test to arrange mocks or verify

borjab
  • 11,149
  • 6
  • 71
  • 98