事务性注释避免了被模拟的服务

时间:2012-10-12 11:22:55

标签: spring testng mockito transactional

我有一个drools规则文件,它使用规则中的服务类。所以一条规则就是这样的:

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

在使用@service和@Transactional(propagation = Propagation.SUPPORTS)注释的验证服务中,drools文件用于statelessKnowledgebase,并添加应在流氓中使用的事实。完成后,调用session.execute(fact)并启动规则引擎。

为了测试规则,我想将countryService.getCountryById()存根。使用mockito没什么大问题。对于使用drools设置的其他服务也做了这个,它工作正常。然而,在这种特殊情况下,countryService没有存根,我无法弄清楚原因。花了很多时间并检查我的代码后,我发现在服务之上使用@Transactional或缺少这个注释会产生差异。缺少@Transaction使mockito模拟了countryservice没有任何问题,让@transactional到位导致mockito失败(没有任何错误或提示)注入模拟以便使用原始的countryservice对象。

我的问题是为什么这个注释会导致这个问题。为什么在设置@Transactional时无法模拟注入模拟?我注意到mockito失败,因为当我调试并检查countryService时,当它作为全局添加到drools会话时,当我在debugwindow中检查countryservice时,我看到以下区别:

  • 使用@transactional:countryService的值为CountryService $$ EnhancerByCGLIB $$ b80dbb7b

  • 没有@transactional:countryService的值为CountryService $$ EnhancerByMockitoWithCGLIB $$ 27f34dc1

除了@transactional之外,我发现了countryservice方法中的断点getCountryById并且调试器在该断点处停止,但是没有@transactional我的断点被跳过,因为mockito会绕过它。

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

  }

测试类:

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

  }

如果我想保留这个@transactional注释但是也能够存根乡村服务方法,那该怎么办?

的问候,

迈克尔

5 个答案:

答案 0 :(得分:8)

发生的事情是您的ValidationService被包装在JdkDynamicAopProxy中,因此当Mockito将模拟注入服务时,它不会看到任何字段将其注入。你需要做两件事之一:

  • 放弃启动Spring Application Context并测试 验证服务,强制您模拟每个依赖项。
  • 或者从JdkDynamicAopProxy中解包您的实现,并自己处理注入模拟。

代码示例:

@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

答案 1 :(得分:8)

请注意,自Spring 4.3.1起,ReflectionTestUtils应自动解包代理。所以

ReflectionTestUtils.setField(validationService, "countryService", countryService);
即使您的countryService注明了@Transactional@Cacheable ...(也就是说,在运行时隐藏在代理服务器后面),

现在应该可以正常工作了

相关问题:SPR-14050

答案 2 :(得分:3)

基于the answer of SuperSaiyen,我创建了一个插入式实用程序类,使其更简单&amp;类型安全:

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

用法很简单,就在测试方法的开头,使用要注入模拟的bean调用方法mockFieldOnBean(Object beanToInjectMock, Class<T> classToMock),以及应该模拟的对象的类。例如:

让我们假设您有一个类型为SomeService的bean,其中包含SomeOtherService的自动装配的bean,类似于;

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

  // some other stuff
}

要在someOtherService bean上模拟SomeService,请使用以下命令:

@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();
  }
}

一切都应该按照自己的意愿运作。

答案 3 :(得分:2)

另一种解决方案是在Spring将所有内容连接在一起之前将模拟对象添加到Spring上下文中,以便在测试开始之前将其注入。修改后的测试可能如下所示:

@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
    // ...

@Primary注释很重要,确保您的新模拟CountryService具有注入的最高优先级,替换正常注释。但是,如果在多个地方注射课程,这可能会产生意想不到的副作用。

答案 4 :(得分:0)

Spring Test模块中存在一个名为AopTestUtils的Spring实用程序。

public static <T> T getUltimateTargetObject(Object candidate)

获取提供的候选对象的最终目标对象, 不仅展开顶级代理,而且展开任何数量的嵌套 代理。如果提供的候选对象是Spring代理,则最终 所有嵌套代理的目标将被返回;否则, 候选人将被退回。

您可以在测试过程中注入模拟或间谍并取消该类的代理以安排模拟或验证