Transactional annotation avoids services being mocked

旧城冷巷雨未停 提交于 2019-11-27 05:09:17

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;
}

Blog entry on the issue

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

Utku Özdemir

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.

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!