Spring context w zastosowaniu testów (Mockito)

0

Cześć,
Chciałem wejść w temat testów ogólnie w Springu, a szczególnie w temat Mockito.
Aby przejść do testowania należy mieć określony plik .xml z contextem aplikacji. W mojej strukturze mam plik* /WEB-INF/dispatcher-servlet.xml*, który zawiera same informacje i beany konfiguracyjne, np. handlerMapping, entityManagerFactory itd. Mam też plik resources/spring/application-config.xml, który został mi automatycznie dorzucony przez STS i nie zawiera w sumie nic, poza suchą definicją XMLa samego w sobie. Korzystając z wątku: http://stackoverflow.com/questions/16458754/spring-web-mvc-dispatcher-servlet-xml-vs-applicationcontext-xml-plus-shared dowiedziałem się, że taki właśnie podział na oba pliki powinien może istnieć. Wiadomo, to tylko nazwy, ale chodzi bardziej o logiczne rozdzielenie zawartości obu plików.

Przechodząc do właściwego pytania - moja klasa testująca:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring/application-config.xml")
public class GetBookTest {
	@Autowired
	private BookService bookService;
	@Test
	public void getSingleBookShouldReturnBook() {
	    Object result = bookService.findById(1);
...

Pierwszym problemem przy najprostszym wywołaniu service jest NullPointer na linijce: Object result = bookService.findById(1);chociaz obiekt o takim id istnieje, zresztą próbowałem na różnych. Czy może być to związane z niepoprawnym określeniem kontekstu? Nie ma tam żadnych beanów, bo nie miałem potrzeby tworzenia takowych w application-context.xml. Też ciężko jest wskazać na lokalizację dispatcher-servlet.xml, której nijak nie chce Spring załapać. W takim układzie czy przenieść gdzieś indziej dispatcher-servlet,xml, czy zmodyfikować w jakiś sposób application-context.xml?

Aha i zapomniałem o jednym fakcie: NullPointer dostaję bez @Autowired, po dodaniu ten adnotacji krzyczy:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'biblioteka.GetBookTest': Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: private pl.test.library.service.BookService biblioteka.GetBookTest.bookService; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [pl.test.library.service.BookService] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

Po dodaniu do application-context.xml jedynej deklaracji, która tam teraz się znajduje:

<context:component-scan base-package="pl.test.library"/> 

Dostaję komunikat Failed to load ApplicationContext.

4

Pomieszałeś temat testów jednostkowych (Mockito) oraz testów integracyjnych (Spring Testing).

Jeżeli chcesz testować jednostkowo nie potrzebujesz ładować testów Spring, wystarczy samo Mockito. I mockowanie zależności (DI).

Jeżeli chcesz testować integracyjnie (Spring Testing) to nie chcesz używać mockito, a pracować z prawdziwymi obiektami.

Właściwie to co chcesz zrobić?

1

jw. jeśli to maja być unit testy to nie potrzebujesz ladować żadnych kontekstów. Np. Dla EasyMocka (którego osobiście polecam :P) wyglądałoby to tak:

@Named
class Something{
    @Inject
    private Service service;
 
    public int method(){
        return service.f();
    }
}

I test:

@RunWith(EasyMockRunner.class)
public class SomethingTest {
    @TestSubject
    private final Something something = new Something();

    @Mock
    private Service service;

    @Test
    public void testMethod(){
        int expectedResult = 1;
        expect(service.f()).andReturn(expectedResult).once();
        replay(service);

        int result = something.method();

        verify(service);
        assertEquals(expectedResult, result);
    }

Można też użyć innego runnera (np. jeśli używamy powermocka i potrzebujemy startować z PowerMockRunner.class) ale wtedy trzeba dorzucić

    @Before
    public void setUp() {
        EasyMockSupport.injectMocks(this);
    }
0

Czyli jestes fanem field injection?

3

Tak bo to jedyne rozwiązanie które realnie wspiera enkapsulacje i nie wymusza zaśmiecania kodu szczegółami które nie są w żaden sposób istotne. Milion setterów albo konstruktory na tysiąc argumentów wyglądają brzydko i zwyczajnie zaciemniają kod.

0

Setery to sie zgadzam, ale jesli masz w klasie tyle zaleznosci, ze masz milion parametrow to cos jest nie tak. Ale o tym juz wielu wiele napisalo. Dobrze wiedziec jak w CERN ucza pisac.

Co do zasmiecania kodu masz racje, polecam przyjrzec sie Lombokowi. Mozesz napisac np. tylko pola prywatne finalne, a konstruktor dla ciebie bedzie automatycznie wygenerowany.

0

Ja oczywiście trochę przesadzam z tym milionem parametrów, niemniej uważam że w klasie serwisowej, której sam i tak nie tworze, posiadanie konstruktora jest zwyczajnie bez sensu i zaśmieca kod tak samo jak settery. Po co mi kod którego nigdy nie używam? Im mniej kodu tym lepiej.
Poza tym może i mam trochę taką manierę żeby stosować SRP gdzie sie da i potem moje klasy korzystają z wielu małych, specjalizowanych, łatwo testowalnych klas, co niestety przekłada się jednocześnie na to że mają dodatkowe zależności. Wolę wstrzyknąć sobie 10 mini klas na 40 linijek każda niż zrobić jednego wielkiego klocka na 1000 linii ;]

A co do tego jak "uczą" w CERNie pisać to w ramach ciekawostki na przykład:
https://twiki.cern.ch/twiki/bin/view/CMSPublic/SWGuidePythonTips#Running_on_more_than_255_files
czyli jak ominąć "problem" limitu argumentów funkcji w pythonie który wynosi 256 :D Fizycy powinni mieć jakiś zakaz programowania ;]

edit: Robimy tu mega offtop. Ja chętnie na ten temat podyskutuje, więc jeśli cię to jakoś bardziej interesuje to zrób o tym nowym wątek gdzieś w Inżynierii Oprogramowania ;)

0

@margor90 - mozesz wyjasnic? Jak masz bezargumentowy konstruktor, to gdzie tam constructor injection?

Co do field injection - ja mam z tym taki problem, ze jesli sie nie uzywa kontenera DI to nie da sie obiektu takiej klasy stworzyc zwyklym new. Nie mozna miec pol final, a ja bardzo lubie final.

@Shalom - a to trzeba podawac to jako pojedyncze argumenty? Nie mozna jednej listy czy jakiegos tam iterable przekazac? Do glowy by mi nie przyszlo cos takiego, no ale ja glupi jestem.

1

@margor90 dobrze prawi.

Przy Mockito miałbyś coś podobnego:

Mockito.when(bookService.findById(Mockito.eq(1))).thenReturn(expectedResult);

Mockito możesz dać jako static import.

Do asercji polecam AssertJ -> assertThat(actual).isEqualTo(something) itp.

0

@margor90 - mozesz wyjasnic? Jak masz bezargumentowy konstruktor, to gdzie tam constructor injection?

Chodziło mi, że coś takiego się nie zadeploy'uje:

@Stateless
@LocalBean
class MyEjbService {
  
    private Srv1 srv1;
    private Srv2 srv2;

    @Inject
    public MyEjbService(Srv1 srv1, Srv2, srv2) {
    }
    
    // business logic

}

Takie coś już tak:

@Stateless
@LocalBean
class MyEjbService {
  
    private Srv1 srv1;
    private Srv2 srv2;
 
    public MyEjbService() {
    }

    @Inject
    public MyEjbService(Srv1 srv1, Srv2, srv2) {
    }
    
    // business logic

}

Takie coś też, bo konstuktor jest trywialny i wygeneruje się sam (dlatego wolę field injection, wedle zasady YAGNI):

@Stateless
@LocalBean
class MyEjbService {
  
    @Inject
    private Srv1 srv1;
    @Inject
    private Srv2 srv2;
    
    // business logic

}
0

Chcesz mi powiedziec, ze EJB 3.x nie wspieraja constructor injection bez dodatkowego no-arga? Pierwsze slysze, ale nie pisalem w Java EE juz od dawna.

H1ghlander napisał(a):

Przy Mockito miałbyś coś podobnego:

Mockito.when(bookService.findById(Mockito.eq(1))).thenReturn(expectedResult);

Mockito możesz dać jako static import.

Do asercji polecam AssertJ -> assertThat(actual).isEqualTo(something) itp.

WAT? A co to ma wstrzykiwania?

0
ucilala napisał(a):

Chcesz mi powiedziec, ze EJB 3.x nie wspieraja constructor injection bez dodatkowego no-arga? Pierwsze slysze, ale nie pisalem w Java EE juz od dawna.

http://stackoverflow.com/questions/9173592/can-i-use-cdi-constructor-injection-for-ejbs

Zdaje się, że specyfikacja tego wymaga (The EJB 3.1 spec, section 4.9.2 Bean Classes). Jak wiadomo specyfikacja, a implementacja to dwie różne rzeczy, więc co serwer aplikacyjny może być inaczej. Więc bezpiecznie jest dodawać bezargumentowy konstruktor (kiedyś mi się nie deploy'owalo) lub robić field injection.

Uznałem, że jak używam field injection to nie ma żadnego problemu, zwłaszcza że Mockito bez problemu nagina rzeczywistość na potrzeby testów (refleksja).

0

To jest dopiero WTF. Nie dziwie sie ze jak sie pisze w EE to pozniej sie wymysla dziwne rzeczy ;d
No, ale na powaznie, nie spodziewalem sie ze jest az tylu (wszyscy tutaj poza mna, wykazane empirycznie poki co) proponentow field injection. Ale kazdy robi to co lubi. Peace.

0

Ciekawe, w watku SO ktory podlinkowales wypowiada sie nie kto inny jak Pete Muir of CDI fame i twierdzi, ze to jest bug jesli CDI jest rowniez 'wlaczone' dla takich EJB (zakladam ze chodzi o to, za jar z tymi EJB ma rowniez META-INF/beans.xml czy jakos tak to bylo).

0

@margor90 - no Ok, ale sam sobie przeczysz. Napisales ze c-tor injection wymaga dodatkowych beanow, ale to nie wina c-tor injection tylko durnych specyfikacji. W watku ktory linkowales Pete powiedzial ze JBoss (Wildfly?) nie wymaga tego, i pewnie nie ma technicznych wymagan, tylko jakies tam przestarzale zdanie z wpecyfikacji sprzed CDI, ktorego zapomnieli skasowac.
Tylko mnie utwierdza w przekonaniu ze Spring to jednak lepszy wybor ;d

0

Spring tez nie jest doskonaly. Nie obsluguje np. pol obiektow dla bezstanowych singletonow w prosty sposob i ludzie robia cuda na kiju ze scope prototype, aby ograniczyc np. liczbe watkow jaka moze pracowac jednoczesnie na bezstanowym singletonem. Dla malych projektow nie ma to znaczenia.

Poza tym WildFly to jest dosc dziwny serwer. Domyslnie jest skonfigurowany tak, ze nie ma tam pol obiektow tylko bezstanowe singletony (jak w Springu).

Dla mnie osobiscie nie ma wiekszego znaczenia czy pracuje ze Springiem czy JEE. Doceniam Springa za mnogosc projektow i bajerow jakich nie ma w JEE. I przede wszystkim za Spring Testing. Jednak aby zrobic cos na szybko to JEE7 nadaje sie swietnie (ale to nie dzial flame).

0

Co to jest 'pol obiektow'? Masz na myslic object pool, czyli pule?
Po co pule bezstanowych singletonow? Jelsi masz ich kilka, to co to za singleton? Jesli cos jest bezstanowe, to po co robic pule, skoro jeden obiekt wystarczy bez wzgledu na to ile jest watkow - w koncu jest bezstanowy?

0

Co to jest 'pol obiektow'? Masz na myslic object pool, czyli pule?

Tak.

Jesli cos jest bezstanowe, to po co robic pule, skoro jeden obiekt wystarczy bez wzgledu na to ile jest watkow - w koncu jest bezstanowy?

Serwery maja skonczona liczbe zasobow. Kazdy nowy watek to jest jakies obciazenie. Pula pozwala ograniczyc ilosc klinetow jaka korzysta z danej uslugi (service): bo np. jednoczesnie usluge moze swiadczyc tylko 70 obiektow bezstanowych. Jak brakuje zasobow, klient czeka. W przypadku, gdy zasob jest nie tak duzo (bo np. obciazenie jest ogromne) dzieki puli klient poczeka az bedzie wolny bezstanowy obiekt.

W przypadku singletona zacznie dzialac i moze byc trudniej ograniczac zuzycie zasobow (niespodziwany przyrost obciazenia jak niebezpiecznie wzrosnie liczba klientow).

Zgadzam sie, ze generalnie dla malo / zwyczajnie obciazonych serwerow pula @stateless / @Singleton nie ma znaczenia. Tam gdzie sa problemy ze skalowalnoscia to moze miec znaczenie. Tak samo jak liczba jednoczesnie uruchamianych asynchronicznych wywolan (nowe watki).

0

Polecam Spocka zamiast mockito,junit,assertj

0

Nie rozumiem co chcesz powiedziec. Jelsi server moze ograniczac liczbe watkow dla 70 obiektow, to dla 1 nie moze? Nie moze ustalic ze 'powiadam, ten jeden jedyny singleton prawdziwy bezstanowy moze byc uzywany przez ile watkow sie da, ale tych watkow nie utworze wiecej niz 70'?
Czy twierdzisz, ze majac 70 obiektow bezstanowych serwer nie musi tworzyc wielu watkow, bo ma juz przeciez 70 obiektow? He? Cos krecisz.

0
NoZi napisał(a):

Polecam Spocka zamiast mockito,junit,assertj

Podpisuje sie pod tym, dodajac iz Spring Test wspiera Spocka!

0

Nie jestem autorytetem w kwestii EJB i ani Springa (ucze sie jednego i drugiego). Ale moge podeslac ciekawy artykul:
http://www.adam-bien.com/roller/abien/entry/do_we_need_stateless_session

Istotny cytat:

The only thing to remember is: every transaction runs in a dedicated Session Bean instance and thread.

Konkludujac, przekombinowuje, tu chodzi o wygode.

So pooling is not needed for performance or scalability reasons, rather than for the ease of use. You can easily restrict the scalability and you get a single threaded programming model for free. In day to day development you don't have to thing about pooling or instance management.

http://www.adam-bien.com/roller/abien/entry/is_ejb_3_the_solution

W Springu na pewno tez mozna to skonfigurowac, czy latwiej watpie.

0

Chciałbym zapytać o Spocka... który naprawdę wygląda ślicznie.

Spock to jest niby BDD. Czy to prawda, że BDD is TDD done right?

tu jest fajny tutek http://thejavatar.com/testing-with-spock/

0

Ok, temat trochę uciekł w bok a nie chcę zakładać nowego, bo w sumie moje kolejne pytanie mieści się w obrębie oryginalnego pytania, otóż - mam kontroler:

@RequestMapping(value = "/bookDetails/{bookId}")
	public String detBook(@PathVariable("bookId") int bookId, @ModelAttribute("book") ShowBookCommand command) {

		command.setBookDto(BookMapper.map(bookService.findById(bookId)));
		command.setBorrowers(BorrowerMapper.map(borrowerService.findAll()));

		return "bookDetails";
	}

ShowBookCommand jest klasą, która przechowuje zarówno obiekt książki (BookDto) jak i listę wypożyczających.
Chcąc napisać pod to test jednostkowy z użyciem MockMVC w podany sposób:

@Mock
	private BookService bookService;

	@Mock
	private BorrowerService borrowerService;

	@Mock
	View mockView;
	
	@InjectMocks
	private DetailsBookController detailsBookController;

	private MockMvc mockMvc;

	Borrower borrower;
	
	@BeforeTest
	public void setup() {

		MockitoAnnotations.initMocks(this);
		this.mockMvc = MockMvcBuilders.standaloneSetup(detailsBookController).setSingleView(mockView).build();
	}

	@Test
	public void testCreateBook() throws Exception {
		List<Borrower> borrowerList = Arrays.asList(borrower, borrower, borrower);
		when(bookService.findById(999)).thenReturn(new Book());
		when(borrowerService.findAll()).thenReturn(borrowerList);
		 
		 this.mockMvc.perform(post("/bookDetails/{bookId}", "999"))
         .andExpect(status().isNotFound())
         .andExpect(view().name("bookDetails"));
	}

Dostaję komunikat:

java.lang.AssertionError: No ModelAndView found

Pisząc ten test wzorowałem się dwóch tutkach:
http://www.luckyryan.com/2013/08/24/unit-test-controllers-spring-mvc-test/
http://myshittycode.com/2014/01/16/mockmvc-mockito-epic-tests/

0
  1. Dlaczego oczekujesz statusu isNotFound?
  2. Wysyłanie danych do widoku poprzez przyjmowanie jako argumentu metody kontrolera @ModelAttribute to terroryzm. To służy do ODBIERANIA danych z formularza na stronie, a nie do wysyłania danych do widoku. Faktem jest że można to "przy okazji" w ten sposób wykorzystać, ale nie znaczy że to dobra praktyka. Po to jest klasa ModelAndView którą generalnie sie zwraca z nie-restowego kontrolera, zamiast jakiegoś stringa...
0
  1. Fakt, poprawiłem.
    Zauważyłem też, że miałem zły adres (bez /action na który zapięta jest cała klasa). Aktualny kod:
this.mockMvc.perform(post("/action/bookDetails/{bookId}", "999"))
         .andExpect(status().isOk())
         .andExpect(view().name("bookDetails"));

Wyrzuca:

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.NullPointerException

na linijkę:

this.mockMvc.perform(post("/action/bookDetails/{bookId}", "999"))

  1. Takie wymaganie, że musi zwracać Stringa. Wcześniej miałem zrobione na ModelAndView właśnie. Niemniej we wklejonych przeze mnie tutkach też jest String
1

No i czego oczekujesz? Zapnij się debugerem i zobacz co jest nullem.

0

Chciałem uzyskać nieprawidłową walidację przy JSR 303. W klasie testującej daje:

@Mock
	private Validator validator;
...
doAnswer(new Answer<Object>() {
            public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
                Errors errors = (Errors) invocationOnMock.getArguments()[1];
                errors.reject("forcing some error");
                return null;
            }
        }).when(validator).validate(anyObject(), any(Errors.class));

Na kontrolerze:
... result.hasErrors()...
Problem jest taki, że nie generuje tego błędu. Zaznaczam, że nie mam własnego validatora tylko korzystam z domyślnego. Ktoś kojarzy jak zmusić to przesłania błędu walidacji?

1 użytkowników online, w tym zalogowanych: 0, gości: 1