Транзакционная аннотация позволяет избежать посмешища сервисов
У меня есть файл правил drools, который использует классы обслуживания в правилах. Итак, одно правило делает что-то вроде этого:
eval (countryService.getCountryById (1)! = null)
В сервисе проверки, который аннотирован @service и @Transactional(распространением =Propagation.SUPPORTS), файл drools используется в базе данных без состояний и добавляются факты, которые должны использоваться в drool. После этого вызывается session.execute(факты) и запускается механизм правил.
Чтобы протестировать правила, я бы хотел заглушить countryService.getCountryById(). Нет больших проблем с использованием mockito. Сделано это для других сервисов, которые также используют настройку drools, и она работала нормально. Однако в данном конкретном случае служба CountryService не была заглушена, и я не мог понять, почему. Потратив много времени и проверив мой код, я обнаружил, что наличие @Transactional над сервисом или отсутствие этой аннотации имеет значение. Отсутствие @Transaction заставило mockito издеваться над деревенским сервисом без каких-либо проблем, а наличие @transactional привело к сбою mockito (без какой-либо ошибки или подсказки), в результате которого было введено макет, так что использовался оригинальный объект countryservice.
Мой вопрос, почему эта аннотация вызывает эту проблему. Почему mockito не может вводить макеты, когда установлен @Transactional? Я заметил, что mockito терпит неудачу, так как когда я отлаживаю и проверяю countryService, когда он добавляется как глобальный в сеанс drools, я вижу следующее отличие, когда я проверяю 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, но также иметь возможность заглушить методы countryservice?
С уважением,
Майкл
4 ответа
Обратите внимание, что начиная с Spring 4.3.1, ReflectionTestUtils должен автоматически разворачивать прокси. Так
ReflectionTestUtils.setField(validationService, "countryService", countryService);
теперь должен работать, даже если ваш countryService
помечен @Transactional
, @Cacheable
... (то есть скрытый за прокси во время выполнения)
Связанная проблема: SPR-14050
Что происходит, так это то, что ваш 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;
}
Основываясь на ответе 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)
с бобом, в который вы хотите внедрить макет, и класс объекта, который должен быть смоделирован. Пример:
Допустим, у вас есть бин с типом SomeService
который содержит боб SomeOtherService
, что-то вроде;
@Component
public class SomeService {
@Autowired
private SomeOtherService someOtherService;
// some other stuff
}
Издеваться someOtherService
на 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();
}
}
все должно работать как надо.
Альтернативное решение - добавить фиктивный объект в контекст 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 имеет высший приоритет для инъекции, заменяя обычный. Однако это может привести к непреднамеренным побочным эффектам, если класс вводится в нескольких местах.
В модуле Spring Test существует утилита Spring под названием AopTestUtils.
public static <T> T getUltimateTargetObject(Object candidate)
Получите конечный целевой объект предоставленного объекта-кандидата, развернув не только прокси верхнего уровня, но также любое количество вложенных прокси. Если предоставленный кандидат является прокси Spring, будет возвращена конечная цель всех вложенных прокси; в противном случае кандидат будет возвращен как есть.
Вы можете ввести mock или spy и отключить класс во время теста, чтобы организовать mock или проверить