spring 事务性注解可避免服务被模仿

oyjwcjzk  于 2022-12-02  发布在  Spring
关注(0)|答案(5)|浏览(117)

我有一个drools规则文件,它在规则中使用服务类。因此,一个规则的作用如下:
(1)返回值为空的值。
在使用@service和@Transactional(propagation=Propagation.SUPPORTS)注解的验证服务中,drools文件用于无状态知识库中,并且添加了应该在drool中使用的事实。完成此操作后,将调用session.execute(facts)并启动规则引擎。
为了测试这些规则,我想stub countryService.getCountryById()。使用mockito没有大问题。在其他使用drools设置的服务上也是这样做的,它运行得很好。但是在这个特殊的例子中,countryService没有被存根,我不知道为什么。在花了很多时间检查我的代码后,我发现有@缺少@Transaction使得mockito模拟countryservice没有任何问题,而使用@transactional则导致mockito注入模拟失败(没有任何错误或提示),因此使用了原始的countryservice对象。
我的问题是为什么这个注解会导致这个问题。为什么当@Transactional被设置时mockito不能注入mock?我注意到当我调试和检查countryService时mockito失败了当它被作为全局添加到drools会话中时我在调试窗口中检查countryService时看到了以下不同:

  • 使用@transactional:国家/地区服务的值为国家/地区服务$$EnhancerByCGLIB$$b80dbb7b
  • 没有@事务性:国家服务的值为国家服务$$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注解,但同时又能stub countryservice方法,你知道该怎么做吗?
敬祝商祺,
米迦勒

64jmpszr

64jmpszr1#

请注意,从Spring 4.3.1开始,ReflectionTestUtils应该自动解包代理。

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

即使您的countryService是使用@Transactional@Cacheable ...(即在运行时隐藏在代理后面)注解的,现在也应该可以工作了
相关问题:SPR-14050

xxls0lw8

xxls0lw82#

发生的情况是ValidationService被 Package 在JdkDynamicAopProxy中,因此当Mockito将模拟注入到服务中时,它看不到任何要注入它们的字段。您需要执行以下两项操作之一:

  • 前台启动Spring应用程序上下文,只测试Validation Service,迫使您模拟每个依赖项。
  • 或者从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;
}
oxcyiej7

oxcyiej73#

基于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<T> classToMock)。示例:
假设您有一个类型为SomeService的bean,它包含一个自动连接的bean SomeOtherService,类似于;

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

一切都应该正常工作。

nom7f22z

nom7f22z4#

另一种解决方案是在Spring将所有东西连接在一起之前,将mock对象添加到Spring上下文中,这样在测试开始之前就已经注入了look对象。

@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具有注入的最高优先级,从而取代普通的CountryService。但是,如果在多个位置注入类,这可能会产生意想不到的副作用。

nr9pn0ug

nr9pn0ug5#

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

public static <T> T getUltimateTargetObject(Object candidate)

获取提供的候选对象的最终目标对象,不仅可以展开一个顶层代理,还可以展开任意数量的嵌套代理,如果提供的候选对象是Spring代理,则返回所有嵌套代理的最终目标;否则,将按原样返回候选项。
您可以注入一个mock或spy,并在测试期间取消代理该类以安排mock或验证

相关问题