Транзакционная аннотация позволяет избежать имитации сервисов

У меня есть файл правил слюни, который использует классы обслуживания в правилах. Итак, одно правило делает что-то вроде этого:

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

В сервисе проверки, который аннотируется @service и @Transactional (ropagation = Propagation.SUPPORTS), файл слюни используется в базе знаний без сохранения состояния, и добавляются факты, которые следует использовать в слюне. После этого вызывается session.execute (факты) и запускается механизм правил.

Чтобы проверить правила, я хотел бы заглушить countryService.getCountryById (). Нет большой проблемы с использованием mockito. Сделано это для другой службы, которая также использует настройку слюни, и она работала нормально. Однако в этом конкретном случае countryService не был заглушен, и я не мог понять, почему. Потратив много времени и проверив свой код, я обнаружил, что наличие @Transactional над сервисом или отсутствие этой аннотации имеет значение. Отсутствие @Transaction заставило mockito имитировать countryservice без каких-либо проблем, наличие @transactional на месте привело к сбою mockito (без каких-либо ошибок или подсказок), вводящего макет, поэтому использовался исходный объект countryservice.

У меня вопрос, почему эта аннотация вызывает эту проблему. Почему mockito не может вводить макеты, если установлен @Transactional? Я заметил, что mockito не работает, так как когда я отлаживаю и проверяю countryService, когда он добавляется как глобальный в сеанс слюни, я вижу следующую разницу, когда проверяю 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, но при этом иметь возможность заглушить методы обслуживания страны?

С уважением,

Майкл


person Michael    schedule 12.10.2012    source источник
comment
Не могли бы вы уточнить, как узнать, почему mockito терпит неудачу? Также, хотя это не связано с проблемой, вы должны отметить, что насмешливое значение на самом деле не рекомендуется, вместо этого вам следует создать экземпляр значения самостоятельно, возможно, с помощью настраиваемой фабрики в вашем тесте или частного конструктора и т. Д.   -  person Brice    schedule 12.10.2012
comment
Также не могли бы вы показать немного больше BaseTestDomainIntegration и, возможно, конфигурацию пружины, если это актуально.   -  person Brice    schedule 12.10.2012
comment
привет брис, я добавил больше информации. увидеть пули   -  person Michael    schedule 12.10.2012
comment
basetestdomainintegration - это то место, где я настраиваю свой spring testcontext. Он помечен @TransactionConfiguration (defaultRollback = true) @ContextConfiguration (locations = {classpath: domain-TestContext.xml})   -  person Michael    schedule 12.10.2012
comment
В вашем контексте есть ли у вас CountryService bean? В таком случае вы можете вместо этого создать макет в контексте Spring. Я считаю, что комбинация базового класса Spring / some aspect / testng может вызвать некоторое дополнительное поведение после создания макета, который в вашем случае я Подозреваемый - это замена mockito mock.   -  person Brice    schedule 12.10.2012
comment
У validationService есть частный член countryService с @Autowired, а контекст Spring использует ‹context: component-scan base-package = my.com /› и ‹context: annotation-config /› config. Поэтому я сам не создаю countryService, но я аннотирую countryservice с помощью Autowired и издеваюсь над countryservice в моем тестовом классе, как вы можете видеть, который вводится через injectMocks   -  person Michael    schedule 12.10.2012
comment
Извините, я хотел сказать обратное в предыдущем комментарии, т.е. вы можете создать макет CountryService в контексте spring , если его еще нет (как вы только что сказали). Другой вариант - выполнить код вашего метода before в самом методе тестирования. Также обратите внимание, что @InjectMocks, которая является аннотацией mockito, даже не заботится об аннотации @Autowired Spring, она ищет только тип и имя поля.   -  person Brice    schedule 12.10.2012
comment
перенос кода в тестовый метод не имеет значения. Я пытаюсь понять, что в транзакционной может иметь отношение к этой проблеме ...   -  person Michael    schedule 12.10.2012


Ответы (5)


Обратите внимание, что начиная с Spring 4.3.1, ReflectionTestUtils должен автоматически распаковывать прокси. Так

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

теперь должно работать, даже если ваш countryService аннотирован @Transactional, _4 _... (то есть скрыт за прокси-сервером во время выполнения)

Связанная проблема: SPR-14050.

person darrachequesne    schedule 13.07.2016

Что происходит, так это то, что ваш 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;
}

Запись в блоге по этой проблеме

person SuperSaiyen    schedule 09.07.2013

Основываясь на ответе 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;
  }
}

Использование простое, просто в начале вашего тестового метода вызовите метод mockFieldOnBean(Object beanToInjectMock, Class<T> classToMock) с bean-компонентом, для которого вы хотите внедрить имитацию, и классом объекта, который должен быть имитируемым. Пример:

Допустим, у вас есть компонент типа SomeService, который содержит автоматически подключенный компонент SomeOtherService, что-то вроде;

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

все должно работать как надо.

person Utku Özdemir    schedule 27.12.2015

Альтернативным решением является добавление фиктивного объекта в контекст 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 имеет наивысший приоритет для внедрения, заменяя обычный. Однако это может иметь непредвиденные побочные эффекты, если класс вводится в нескольких местах.

person pimlottc    schedule 22.04.2015

Существует утилита Spring под названием AopTestUtils в модуле Spring Test.

public static <T> T getUltimateTargetObject(Object candidate)

Получите конечный целевой объект предоставленного объекта-кандидата, развернув не только прокси верхнего уровня, но также любое количество вложенных прокси. Если предоставленный кандидат является прокси-сервером Spring, будет возвращена конечная цель всех вложенных прокси-серверов; в противном случае кандидат будет возвращен как есть.

Вы можете ввести mock или spy и отключить класс во время теста, чтобы организовать mock или проверить

person borjab    schedule 01.06.2020