2012-10-12 12 views
7

Mam plik reguł drools, który używa klas usług w regułach. Więc jedna reguła robi coś takiego:Adnotacja transakcyjna pozwala uniknąć drwiania usług

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

W validationservice że jest opatrzone @service i @Transactional (propagacja = Propagation.SUPPORTS) plik Drools jest wykorzystywany w bazie bezpaństwowej wiedzy i dodawane są fakty, które powinny być używane w ślinotokach. Po wykonaniu tej czynności wywoływana jest sesja.execute (fakty) i uruchamia się silnik reguł.

Aby przetestować reguły, chciałbym dodać nazwę countryService.getCountryById(). Nie ma większego problemu z używaniem mockito. Zrobiłem to dla innych usług, które używają również konfiguracji drools i działało dobrze. Jednak w tym konkretnym przypadku usługa countryService nie została przygnieciona i nie mogłem ustalić, dlaczego tak się dzieje. Po spędzeniu dużej ilości czasu i sprawdzeniu mojego kodu stwierdziłem, że posiadanie @Transactional powyżej usługi lub brak tej adnotacji zrobiło różnicę. Brak efektu @Transaction spowodował, że mockito kpi z usługi country bez problemu, dzięki temu, że @transactional na miejscu spowodowało, że mockito nie zdał (bez żadnego błędu lub podpowiedzi) wstrzyknięcia próbnego, więc użyto oryginalnego obiektu countryservice.

Moje pytanie brzmi, dlaczego ta adnotacja powoduje ten problem. Dlaczego nie można wstrzyknąć mockito podczas ustawiania @Transactional? Zauważyłem, że Mockito zawodzi jako kiedy debugowania i sprawdzić countryService gdy jest on dodawany jako globalny sesją ślini widzę następującą różnicę kiedy skontrolować countryservice w moim debugwindow:

  • z @ transakcyjny: countryService ma wartość CountryService $$ EnhancerByCGLIB $$ b80dbb7b

  • bez @transactional: countryService ma CountryService wartość $$ EnhancerByMockitoWithCGLIB $$ 27f34dc1

Ponadto z @t ransactional mój breakpoint w metodzie countryservice getCountryById zostaje znaleziony i debugger zatrzymuje się w tym punkcie przerwania, ale bez @transactional mój punkt przerwania jest pomijany, ponieważ mockito pomija go.

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

    } 

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

    } 

Każdy pomysł, co zrobić, jeśli chcę utrzymać ten @transactional adnotacji ale także móc skrótową z methodes countryservice?

pozdrowienia,

Michael

+0

Mógłbyś być bardziej precyzyjne, skąd wiesz, dlaczego mockito nie działa? Również niezwiązany z tą kwestią, należy pamiętać, że wartość kpiąca nie jest naprawdę zalecana, powinieneś zamiast tego sam stworzyć instancję wartości, może z niestandardową fabryką w swoim teście lub prywatnym konstruktorem, itd ... – Brice

+0

Czy mógłbyś pokaż trochę więcej 'BaseTestDomainIntegration' i może config wiosną, jeśli jest to istotne. – Brice

+0

Witam brice, dodałem więcej informacji. zobacz pociski – Michael

Odpowiedz

4

Co dzieje się swoją ValidationService jest owinięta w JdkDynamicAopProxy, więc kiedy Mockito idzie do wstrzyknąć mocks do służby nie widać żadnych pól, aby wprowadzić je do. musisz zrobić jedną z dwóch rzeczy:

  • zrezygnować rozpoczęciem Kontekst Wiosna aplikacji i egzaminu tylko Obsługi Walidacja , zmuszając do kpić każdy zależność.
  • Lub odtwórz swoją implementację z JdkDynamicAopProxy, a następnie wykonaj wstrzykiwanie próbne.

Kod Przykład:

@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

Alternatywnym rozwiązaniem jest dodanie atrapa obiektu do kontekstu wiosną przed wiosennym przewodów wszystko razem, tak, że będzie już wtryskiwany przed swój zaczynają się testy. Zmodyfikowany Test może wyglądać tak:

@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 adnotacja jest ważne, upewniając się, że nowy mock CountryService ma najwyższy priorytet do wstrzykiwań, zastępując jeden normalny. Może to mieć niezamierzone skutki uboczne, jeśli klasa zostanie wstrzyknięta w wielu miejscach.

1

podstawie the answer of SuperSaiyen, stworzyłem drop-in klasę użytkową, aby uprościć & typ bezpieczne:

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

Użycie jest proste, wystarczy na początku swojej metody badania, należy wywołać metodę mockFieldOnBean(Object beanToInjectMock, Class<T> classToMock) z fasolę, do której chcesz wstrzyknąć próbkę, oraz klasę obiektu, którą należy wyśmiać. Przykład:

Załóżmy, że masz fasolę z typem SomeService, która ma automatyczną fasolę z SomeOtherService, coś w stylu;

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

    // some other stuff 
} 

drwić someOtherService na SomeService fasoli, należy użyć następujących:

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

wszystko powinno działać tak, jak powinny.

4

Należy pamiętać, że od wiosny 4.3.1, ReflectionTestUtils powinien automatycznie rozwijać serwery proxy. Więc

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

powinna teraz działać, nawet jeśli countryService jest opatrzone @Transactional, @Cacheable ... (to jest ukryta za serwerem proxy w czasie wykonania)

Powiązane problem: SPR-14050

Powiązane problemy