2012-10-12 167 views
7

Tengo un archivo de reglas drools que usa clases de servicio en las reglas. Así que una regla hace algo como esto:La anotación transaccional evita que se burlen de los servicios

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

En un validationservice que se anota con @Service y @Transactional (= propagación Propagation.SUPPORTS) el archivo drools se usa en una base de conocimiento sin estado y se agregan hechos que deberían usarse en la baba. Una vez hecho esto, se invoca session.execute (facts) y se inicia el motor de reglas.

Para probar las reglas, me gustaría incluir el countryService.getCountryById(). No es un gran problema usar mockito. Hecho esto para otro servicio que también usa una configuración de drools y funcionó bien. Sin embargo, en este caso particular, countryService no fue interceptado y no pude entender por qué. Después de pasar mucho tiempo y consultar mi código, descubrí que tener @Transactional por encima del servicio o carecer de esta anotación marcaba la diferencia. Al carecer de @Transaction hizo que el mockito se burlara del servicio del país sin ningún problema, tener el @transactional en su lugar causó que el mockito fallara (sin ningún error o sugerencia) inyectando el simulacro para que se usara el objeto de servicio del país original.

Mi pregunta es por qué esta anotación causa este problema. ¿Por qué no se puede mockito inyectar los simulacros cuando se establece @Transactional? Me he dado cuenta de que Mockito está fallando como cuando puedo depurar e inspeccionar el countryService cuando está siendo añadido como global para la sesión drools veo la siguiente diferencia cuando inspeccione el countryservice en mi DebugWindow:

  • con @ transaccional: countryService tiene el valor CountryService $$ EnhancerByCGLIB $$ b80dbb7b

  • sin @Transactional: countryService tiene el valor CountryService $$ EnhancerByMockitoWithCGLIB $$ 27f34dc1

Además con @t mi punto de ruptura se establece en el metodo countryservice getCountryById y el depurador se detiene en ese punto de interrupción, pero sin @transactional mi punto de interrupción se omite ya que mockito lo puentea.

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

    } 

Y la clase de prueba:

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

    } 

Alguna idea de qué hacer si quiero mantener esta anotación @Transactional sino también ser capaz de derivadas La methodes countryservice?

cordiales,

Michael

+0

Podría ser más ¿Preciso cómo sabes por qué Mockito está fallando? Además, aunque no esté relacionado con el problema, debe tener en cuenta que no se recomienda el valor de burla, en su lugar debe crear una instancia de valor, tal vez con una fábrica personalizada en su prueba o un constructor privado, etc. – Brice

+0

También podría mostrar un poco más de 'BaseTestDomainIntegration' y tal vez la configuración de Spring si eso es relevante. – Brice

+0

Hola, brice, he agregado más información. mira las viñetas – Michael

Respuesta

4

qué está sucediendo es que su ValidationService se está envuelto en una JdkDynamicAopProxy, por lo que cuando Mockito va a inyectar los simulacros en el servicio que no ve ningún campo a inyectarlos en. Tendrá que hacer una de dos cosas:

  • Forego iniciar su contexto de aplicación de primavera y la prueba sólo la validación del servicio , lo que obligó a burlarse de cada dependencia.
  • O desenvuelva su implementación desde JdkDynamicAopProxy, y maneje la inyección de los burla usted mismo.

Ejemplo de código:

@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

2

Una solución alternativa consiste en añadir el objeto de burla al contexto primavera antes de la primavera cables todo juntos, de manera que ya se han inyectado antes de su las pruebas comienzan El ensayo modificado podría ser algo como esto:

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

La anotación @Primary es importante, asegurándose de que su nueva maqueta CountryService tiene la máxima prioridad para la inyección, en sustitución de la normal. Sin embargo, esto puede tener efectos secundarios involuntarios si la clase se inyecta en varios lugares.

1

Basado en the answer of SuperSaiyen, he creado una clase de utilidad drop-in para hacerlo más sencillo & tipo de seguro:

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

uso es simple, justo en el comienzo de su método de prueba, llame al método mockFieldOnBean(Object beanToInjectMock, Class<T> classToMock) con el Bean que desea inyectar un simulacro, y la clase del objeto que debe ser burlado. Ejemplo:

Digamos que tiene un bean con tipo SomeService que contiene un bean autoadjunto de SomeOtherService, algo así como;

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

    // some other stuff 
} 

burlarse someOtherService en el frijol SomeService, usa la siguiente:

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

todo debería funcionar como deberían.

4

Tenga en cuenta que, dado que Spring 4.3.1, ReflectionTestUtils debería desplegar automáticamente proxies. Así

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

ahora debería funcionar incluso si su countryService se anota con @Transactional, @Cacheable ... (es decir, escondido detrás de un proxy en tiempo de ejecución)

cuestión relacionada: SPR-14050

Cuestiones relacionadas