2012-10-12 13 views
7

Ho un file di regole di drools che utilizza le classi di servizio nelle regole. Quindi una regola fa qualcosa di simile:L'annotazione transazionale evita che i servizi vengano derisi

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

In un validationservice che viene annotato con @service e @Transactional (propagazione = Propagation.SUPPORTS) il file drools è usato in una statelessKnowledgebase e vengono aggiunti fatti che dovrebbero essere usati nella bava. Fatto ciò, viene richiamato session.execute (facts) e viene avviato il motore di regole.

Per testare le regole, desidero eseguire il stub del paeseService.getCountryById(). Nessun grosso problema con il mockito. Fatto questo per altri servizi che utilizzano un setup di drools e ha funzionato bene. Tuttavia in questo caso particolare il countryService non è stato soppresso e non sono riuscito a capire perché. Dopo aver trascorso un sacco di tempo e verificato il mio codice, ho scoperto che avere @Transactional al di sopra del servizio o la mancanza di questa annotazione ha fatto la differenza. Mancando la @Transaction ha fatto deridere il countryservice senza alcun problema, avendo il @transactional sul posto ha causato il fallimento di mockito (senza alcun errore o suggerimento) iniettando il mock in modo che fosse utilizzato l'oggetto countryservice originale.

La mia domanda è perché questa annotazione causa questo problema. Perché mockito non può iniettare i mock quando è impostato @Transactional? Ho notato che Mockito sta fallendo come quando il debug e ispezionare la countryService quando viene aggiunto come globale alla sessione sbava vedo la seguente differenza quando ho ispezionare il countryservice nel mio DebugWindow:

  • con @ transazionale: countryService ha il valore CountryService $$ $$ EnhancerByCGLIB b80dbb7b

  • senza @Transactional: countryService ha il valore CountryService $$ $$ EnhancerByMockitoWithCGLIB 27f34dc1

Inoltre con @t ransactional il mio punto di interruzione nel metodo countryservice getCountryById viene trovato e il debugger si ferma in quel punto di interruzione, ma senza @transactional il mio punto di interruzione viene saltato mentre mockito lo ignora.

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

    } 

E la classe di test:

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

    } 

Qualsiasi idea di cosa fare se voglio mantenere questa annotazione @Transactional, ma anche essere in grado di stub i Méthodes countryservice?

saluti,

Michael

+0

Potrebbe essere più preciso su come sai perché il mockito sta fallendo? Anche se non è correlato al problema, si dovrebbe notare che il valore di mocking non è veramente raccomandato, si dovrebbe invece creare un'istanza di valore da soli, magari con un factory personalizzato nel test o un costruttore privato, ecc ... – Brice

+0

Inoltre si potrebbe mostra un po 'più di 'BaseTestDomainIntegration' e forse la spring config se è rilevante. – Brice

+0

ciao, ho aggiunto più informazioni. vedere i proiettili – Michael

risposta

4

che cosa sta accadendo è il tuo ValidationService viene avvolto in un JdkDynamicAopProxy, in modo che quando Mockito va per iniettare le prende in giro nel servizio non vede tutti i campi a loro iniettare. Avrai bisogno di fare una delle due cose:

  • Forego iniziare la vostra Contesto Primavera Applicazione e prova solo il Validation Service, ti costringe a prendere in giro ogni dipendenza.
  • Oppure estrai la tua implementazione da JdkDynamicAopProxy e gestisci l'iniezione dei moccioli da solo.

Esempio di codice:

@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 soluzione alternativa è quella di aggiungere l'oggetto fittizio al contesto primavera prima primavera fili tutto insieme, in modo che sarà già stato iniettato prima del tuo i test iniziano Il test modificato potrebbe apparire qualcosa di simile:

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

Il @Primary annotazione è importante, fare in modo che il vostro nuovo CountryService finto ha la priorità assoluta per l'iniezione, in sostituzione di quella normale. Questo può avere effetti collaterali indesiderati, tuttavia, se la classe viene iniettata in più punti.

1

Sulla base di the answer of SuperSaiyen, ho creato una classe di utilità drop-in per rendere più semplice & Tipo sicuro:

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

L'uso è semplice, basta sull'inizio del vostro metodo di prova, chiamare il metodo mockFieldOnBean(Object beanToInjectMock, Class<T> classToMock) con la fagiolo su cui si vuole iniettare un mock e la classe dell'oggetto che dovrebbe essere deriso. Esempio:

Diciamo che avete un fagiolo con il tipo di SomeService che detiene un bean autowired di SomeOtherService, qualcosa di simile;

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

    // some other stuff 
} 

per deridere someOtherService sul fagiolo SomeService, utilizzare il seguente:

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

tutto dovrebbe funzionare come dovrebbero.

4

Si prega di notare che dalla primavera 4.3.1, ReflectionTestUtils dovrebbe scartare automaticamente i proxy. Così

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

dovrebbero ora funzionare anche se il countryService è annotata con @Transactional, @Cacheable ... (cioè, nascosto dietro un proxy in fase di esecuzione)

problema correlate: SPR-14050