【问题标题】:Transactional annotation avoids services being mocked事务注解避免服务被嘲笑
【发布时间】:2012-10-03 04:55:11
【问题描述】:

我有一个drools 规则文件,它在规则中使用服务类。所以一个规则做这样的事情:

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

在使用 @service 和 @Transactional(propagation=Propagation.SUPPORTS) 注释的验证服务中,drools 文件用于无状态知识库中,并添加了应在 drool 中使用的事实。完成后,将调用 session.execute(facts) 并启动规则引擎。

为了测试规则,我想存根 countryService.getCountryById()。使用 mockito 没有什么大问题。为其他使用 drools 设置的服务完成此操作,并且效果很好。但是在这种特殊情况下, countryService 没有被存根,我不知道为什么。在花了很多时间检查我的代码之后,我发现在服务上方使用 @Transactional 或缺少此注释会有所不同。缺少 @Transaction 使得 mockito 可以毫无问题地模拟 countryservice,而使用 @transactional 会导致 mockito 失败(没有任何错误或提示)注入模拟,因此使用了原始的 countryservice 对象。

我的问题是为什么这个注释会导致这个问题。为什么在设置@Transactional 时不能模拟注入模拟?我注意到,当我调试和检查 countryService 时,mockito 失败了,当它作为全局添加到 drools 会话时,当我在调试窗口中检查 countryservice 时,我看到以下差异:

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

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

除了@transactional,我在countryservice方法getCountryById中找到了断点,调试器在该断点处停止,但没有@transactional,我的断点被跳过,因为mockito绕过它。

验证服务:

@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 注释但也能够存根 countryservice 方法,知道该怎么做吗?

问候,

迈克尔

【问题讨论】:

  • 您能否更准确地说明您如何知道 mockito 失败的原因?此外,虽然与问题无关,但您应该注意不建议真正推荐模拟值,您应该自己创建一个值实例,可能在您的测试中使用自定义工厂或私有构造函数等...
  • 你能不能多展示一些BaseTestDomainIntegration 和spring 配置,如果有关系的话。
  • 嗨,brice,我添加了更多信息。看到子弹
  • basetestdomainintegration 是我设置 spring 测试上下文的地方。它带有注释 @TransactionConfiguration(defaultRollback = true) @ContextConfiguration(locations = { "classpath:domain-TestContext.xml" })
  • 在您的上下文中,您有 CountryService bean 吗? 如果是这样,您可能想在 spring 上下文中创建模拟。我相信组合 Spring/some aspect/testng 基类可能会在创建模拟后触发一些额外的行为,在你的情况下,我怀疑是 mockito mock 的替代品。

标签: spring testng mockito transactional


【解决方案1】:

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

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

即使您的 countryService@Transactional@Cacheable... 注释(即在运行时隐藏在代理后面)现在也应该可以工作

相关问题:SPR-14050

【讨论】:

    【解决方案2】:

    发生的事情是您的 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

    【讨论】:

      【解决方案3】:

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

      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&lt;T&gt; classToMock)。示例:

      假设你有一个 SomeService 类型的 bean,它包含一个自动装配的 SomeOtherService bean,类似于;

      @Component
      public class SomeService {
        @Autowired
        private SomeOtherService someOtherService;
      
        // some other stuff
      }
      

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

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

      一切都应该正常工作。

      【讨论】:

        【解决方案4】:

        另一种解决方案是在 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 具有最高的注入优先级,替换正常的。但是,如果将类注入多个位置,这可能会产生意想不到的副作用。

        【讨论】:

          【解决方案5】:

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

          public static <T> T getUltimateTargetObject(Object candidate)
          

          获取提供的候选对象的最终目标对象, 不仅可以解包顶级代理,还可以解包任意数量的嵌套 代理。如果提供的候选人是 Spring 代理,则最终 将返回所有嵌套代理的目标;否则, 候选人将按原样返回。

          您可以在测试期间注入模拟或间谍并取消代理类以安排模拟或验证

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2019-09-08
            • 1970-01-01
            • 2020-11-10
            • 1970-01-01
            • 2018-10-07
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多