Wstrzykiwanie zależności a testy jednostkowe - złoty środek

1
Shalom napisał(a):

A czemu nie? Co jest w takim teście złego? Czy jeśli ktoś przypadkiem w trakcie refaktoringu nam naszego error handlera zakomentuje to test się wywali? Wywali się. Więc jak dla mnie spełnia swoją rolę.
Bo takie myślenie jakie sugeruje między innymi @jarekr000000 że nie ma co testować mocków jest moim zdaniem po prostu śmieszne. Ja rozumiem że robienie assertTrue(true); jest bez sensu, tak samo jak i sprawdzanie czy zaprogramowany mock zwrócił nam wartość którą zaprogramowaliśmy. Ale poza takimi idiotycznymi przypadkami nie widzę za bardzo gdzie jest problem w testowaniu z użyciem mocków, nawet jeśli weryfikujemy tylko czy jakaś metoda została zawołana.

  1. Osobiscie uwazam testowanie wasMethodCalled, za cos nieco tylko lepszego od kolki, sraczki i migreny.

  2. Wyjatek: Jezeli masz gdzies Endpointa i wysylasz cos na zewnetrzny serwis (mail, http, event) fire & forget - to jeszcze taki Mock i testowanie ujdzie.
    Jakkolwiek zawsze sprawdzam czy nie mozna postrawic w tescie malego HTTP servera lub Mail - przeciez to proste! I sprawdzic co naprawde przyszlo. (To jest ogolnie najlepsza metoda - o ile mamy dobrze ogarniete sprawy z security, u mnie w firmie warstwa security jest na wpol magiczna (proxy same cos sprawdzaja i dopisuja do requestow) i czesto sie tak niestety nie da zrobic.

  3. Jezeli natomiast jestes wlascicielem kodu wywolywanej (i sprawdzanej) metody to naonczas tylko wielki WTF. Przy okazji : widze taki kod czesto - to sie nazywa Londynska szkola TDD. W podanym przez Ciebie przypadku zapewne chcialbys testowac czy wywolano dataSelector.filter(accessibleData); (Bo jedynie to wywolanie jest zrypane i nie zalatwione systemem typow u Ciebie :-) (a pisales, w innym watku, ze sie nie da) ).
    Oczywiscie mozna tak testowac - konczy sie to tym, ze ktos kiedys przykladowo poprawia implementacje i robi memoizacje wynikow (Cache). Wtedy nagle sie okazuje, ze zupelnie niewinny test zaczyna sie wywalac (bo juz nie wywoluje metody). Sam sie zastanow co sa warte testy, ktore musisz zmieniac przy kazdym refaktoringu.
    A to jeszcze w miare rozsadny przyklad - najczesciej robimy jakis rozwal w designe i wtedy takie testy pomagaja tyle co dziura w brzuchu (pol czasu strocone zeby sie kompilowaly i nawet po tem nie wiadomo co one robia...).
    Jedyne co taka (londyska szkola TDD) daje to poprawe samopoczucia piszacego - przeciez wszystko zrobione w TDD i 100% pokryte (te tak pare lat temu robilem :-)).
    4.Dodatkowo, w podanym przypadku MagicSerwis narzut (LOC) na zamockowanie i sprawdzenie wszystkich wywolan testami bedzie duzo wiekszy niz po prostu wywolanie metod for real. A sens - jak wyzej.

  4. Ogolnie, czasem pojawia sie pytanie, jak w takim razie testowac metody void? Odpowiedz jest prosta - takie metody nie maja racji bytu. Skoro zwraca void to nic nie robi - usuwamy :-).

1

czy nie mozna postrawic w tescie malego HTTP servera lub Mail - przeciez to proste

Jasne, i w ramach testów integracyjnych na CI można to zrobić, ale testy jednostkowe służą do tego żeby je odpalać lokalnie w 2 sekundy wiedzieć czy coś popsulismy refaktorujac czy też nie. Jeśli testy wykonują się dłużej to 90% developerów zrobi skipTests i w ogóle nie będą ich odpalać.

Sam sie zastanow co sa warte testy, ktore musisz zmieniac przy kazdym refaktoringu.

No przepraszam, ale jeśli zmieniłeś zachowanie metody a twój test się nie wywalił to taki test jest zupełnie bezużyteczny. Dodanie jakiegoś cache to jest zmiana zachowania metody bo teraz może zwracać zupełnie co innego niż wcześniej! Testy jednostkowe mówią czy metoda zachowuje się tak jak powinna i to logiczne że test się nagle wywali - bo powinien! Problem jest kiedy ktoś zrobi mega whiteboxowy test i oczekuje np. że metody są wołane w jakiejs kolejności albo coś równie dziwnego i wtedy nawet zamiana 2 linijek kolejnością wywala test - taki test nie ma sensu. Ale jeśli w swoim kodzie zmieniłeś zachowanie metody a testy nadal zielone, to znaczy ze guzik warte te twoje testy. Co zresztą jest zwykle prawdą dla testów integracyjnych -> to są takie trochę sanity test, sprawdzają tylko czy coś w ogóle działa, czy jakiś ustalony happy path przechodzi, ale nic poza tym.

Jak dla mnie to są 2 pytania które trzeba sobie zadać jeśli zastanawiamy się czy nasz test jest dobry czy nie:

  • czy mogę zmienić strukturę kodu (ale nie jego zachowanie!) tak żeby test się wywalił -> jeśli tak, to test jest zbyt whiteboxowy / za blisko kodu i trzeba go poprawić
  • czy mogę popsuć kod (np. przez podmianę argumentu jakiejś metody na błędny, zakomentowanie jakiegoś brancha czy instrukcji) jednocześnie nie triggerując blędu w teście -> jeśli tak to należy poprawic test lub napisać dodatkowy test który tą sytuacje wykryje

@Afish z tymi code review to mam mieszane uczucia. Trochę jak w takim porzekadle, ze jak pokażesz programiście na review 50 linijek to znajdzie 10 potencjalnych bugów, 5 sugestii gdzie można coś poprawić i 2 wzorce projektowe które można by tam wrzucić. Ale jak pokażesz mu 500 linijek to powie hmm, wygląda ok ;] Szczególnie dla większego systemu moze tak być że ktoś kto reviewuje kod nie do końca ma pojęcie co "biznesowo" ma dany kod robić i analizuje tylko strukturę. W efekcie takiej podmianki na service.method(new A()); może w ogóle nie wyłapać, bo nie będzie wiedział że to z biznesowego punktu widzenia nie ma sensu. Tak samo to ze ktoś wyrzucił z kodu część error handlera - reviewer nie musi wiedzieć że to wcale nie tak miało być ;)

0
Mały Mleczarz napisał(a):

4.Dodatkowo, w podanym przypadku MagicSerwis narzut (LOC) na zamockowanie i sprawdzenie wszystkich wywolan testami bedzie duzo wiekszy niz po prostu wywolanie metod for real.

Bzdura. Testy na mockach odbywają się lokalnie, testy integracyjne wymagają połączenia z serwerem, wykonania polecenia, odebrania wyników. Z praktyki wiem, że testy na mockach wykonują się kilkadziesiąt razy szybciej od testów integracyjnych z bazą danych.

3
Shalom napisał(a):

@Afish z tymi code review to mam mieszane uczucia. Trochę jak w takim porzekadle, ze jak pokażesz programiście na review 50 linijek to znajdzie 10 potencjalnych bugów, 5 sugestii gdzie można coś poprawić i 2 wzorce projektowe które można by tam wrzucić. Ale jak pokażesz mu 500 linijek to powie hmm, wygląda ok ;] Szczególnie dla większego systemu moze tak być że ktoś kto reviewuje kod nie do końca ma pojęcie co "biznesowo" ma dany kod robić i analizuje tylko strukturę. W efekcie takiej podmianki na service.method(new A()); może w ogóle nie wyłapać, bo nie będzie wiedział że to z biznesowego punktu widzenia nie ma sensu. Tak samo to ze ktoś wyrzucił z kodu część error handlera - reviewer nie musi wiedzieć że to wcale nie tak miało być ;)

No jasne, teoria teorią, a rzeczywistość rzeczywistością, ale z takimi argumentami można się posunąć dalej. No bo po co robić silne typowanie i strukturę klas odporną na głupoty, jak przyjdzie ktoś, komu podobają się gołe stringi, pozmienia, a potem review zrobi ktoś, kto nie wie, „że to wcale nie tak miało być”.

Dla mnie Assert.MethodWasCalled zazwyczaj wprowadza więcej problemów, niż daje korzyści — za bardzo testują strukturę kodu, a nie jego efekt, przez co przy zmianach testy się sypią i nie jest jasne, czy testy coś znalazły, czy były zbyt kruche. Jednocześnie jeżeli czujesz, że w Twoim przypadku takie podejście się sprawdza, to tak rób i korzystaj. Rzeczywistość nie jest czarno-biała, Working Effectively with Legacy Code pięknie gada o testowaniu, a w praktyce praca z legacy często sprowadza się do robienia zmian „na czuja”, TDD teoretycznie jest cudowne, a praktycznie często powstaje nieczytelna architektura, za to pięknie pokryta kodem.

1

No jasne, teoria teorią, a rzeczywistość rzeczywistością, ale z takimi argumentami można się posunąć dalej. No bo po co robić silne typowanie i strukturę klas odporną na głupoty, jak przyjdzie ktoś, komu podobają się gołe stringi, pozmienia, a potem review zrobi ktoś, kto nie wie, „że to wcale nie tak miało być”.

Z tego samego powodu dla ktorego piszemy testy - na wszelki wypadek ;) Bo jak mamy silne typowanie, testy i code review to szansa wpadki maleje. Jestem jednak przeciwny takiemu podejściu że tego nie testujemy bo to wyjdzie na code review albo analogicznie tego nie ma co analizować za długo na review bo skomplikowane, ale mamy kupę testów i przechodzą ;)

Dla mnie Assert.MethodWasCalled zazwyczaj wprowadza więcej problemów, niż daje korzyści — za bardzo testują strukturę kodu, a nie jego efekt, przez co przy zmianach testy się sypią i nie jest jasne, czy testy coś znalazły, czy były zbyt kruche

Jasne, dlatego pisałem wyżej że taki test turbo-whitebox który wysypuje się przy zmianie struktury a nie zachowania jest niewiele wart. Ale samo sprawdzenie czy metoda została zawołana moim zdaniem jeszcze niczego takiego nie sugeruje.

Jednocześnie jeżeli czujesz, że w Twoim przypadku takie podejście się sprawdza, to tak rób i korzystaj.

Tyle to ja wiem, trochę w życiu juz kodu naklepałem. Ale tą dyskusje rozpocząłem po to zeby posłuchać i pospierać się w kwestii jak można to zrobić inaczej/lepiej :)

0

Zachecam ogolnie, aby podyskutowac na konkrecie, nieco doszczegolowionym projekcie - takim jak zgloszony przez @Shalom.

https://github.com/javaFunAgain/magic_service_story/tree/30_FIRST_TEST

Kod juz sie kompiluje i przechodzi pierwszy trywialny test.

Mozecie forkowac i zrobic to na DI framework czy jak uwazacie powinno byc slusznie.
( a przy okazji powinien sie dzis pojawic kolejny odcinek)
Ale zanim to bedzie po mojemu wygladalo - to bedzie potrzeba jeszcze z 5 odcinkow (najmniej)...

1

@jarekr000000 mam wrażenie że trochę nie rozumiesz zamysłu @Shalom. Kod jest przykładowy, istotne w nim jest to, że:

  1. ma sporo zależności
  2. zależności te są duże
  3. chcemy to testować

Przynajmniej ja to w ten sposób rozumiem i stąd nie odwołuję się wprost do przedstawionego kodu. Nie ma co narzekać że sie nie kompiluje, srsly, nie o to chodzi. Natomiast podoba mi się pomysł konkretnych rozwiązań - sam bym napisał ale jestem du*a ze Springa i innych DI frameworks do javy.

2

Na spokojnie, to poniżej kwalifikuje jako bzdurę dnia (przepraszam, ale muszę wybuchnąć):
To zależy od tego jak dokładnie brzmiał kontrakt danej metody, który pewnie był luźny i nic nie definiował ;] Wiele osób mogło założyć że metoda zwraca zawsze nową instancje (zresztą widzieli to w kodzie!) i na przykład gdzieś się na tym obiekcie synchronizować, albo stosować jako jakiś klucz w mapie itd. i dodanie cache powoduje masę błędów, mimo że teoretycznie nic się nie powinno stać ;] Zachowanie metody było takie że zawsze alokuje nowy obiekt, a teraz nie zawsze, to znaczy że zachowanie uległo zmianie. - Shalom dzisiaj, 14:17

Modularyzacja - czyli programowanie obiektowe, strukturalne , funkcyjne właśnie polega na tym, że tworzy się różnej postaci konktrakty.
I w Javie taki konkrakt dla metody to konkretnie:

  1. sygnatura
  2. testy
  3. dokumentacja
    W tej kolejności. Można się kłócić na temat kolejności punkt 2 vs 3, ale na pewno:

Nie ma na tej liście implementacji!

Ktokolwiek, zagląda do implementacji i wyciąga daleko idące wnioski jak tego używać - łamie zasady modularyacji i współpracy. Tak się nie robi, powinien wrócić do lat 80 i pisać w BASICU (wtedy tak się robiło (koszmar)).

Cały sens enkapsulacji to przecież ukrycie kodu, żeby się przed nadinterpretacjami uchować. Nie ma, nie zaglądaj, nie twoja sprawa!

Jak zaczynałem programowanie w Javie (2000 rok) java 1.1 to to było w 100 % jasne . A wtedy często się synchronizowało - i wrzucało obiekty do Map (do Hashtable ,bo HashMapy jeszcze nie było).

Czy coś się bardzo od czego czasu z programistami zrypało? ( A wtedy przecież, wcale nie byliśmy dobrzy...).

Kontynuując.

Oczywiście pisząc testy do mojej jakiejś metody X na pewno pownienem przetestować wejście i wyjście (exceptiony to też wyjście).

Implementację (natrętnie) potencjalnie można - ale to droga donikąd.
Jeżeli będę testował (po Londyńsku) poszczególne linie implementacji to to co uzyskuje to usztywnienie konktraktu i implementacji.
Z pozoru mogłoby się wydawać fajne - w praktyce jeżeli czegoś nie trzeba usztywniać to po co?

Może się okazać, że właśnie Cache będzie potrzebny ze względów wydajnościowych - a tu już mamy test, który gwarantuje, że się nic nie cachuje, co gorsza ludzie go widzieli i poszli za tym założeniem. Może się okazać, że już nie trzeba wołać jakiegoś innego serwisu - bo powstał jakiś, alternatywny - lepszy.

Po to jest kod składowany w pamięciach modyfikowalnych (a nie na kamiennych tablicach), żeby go można było ruszać.

Ogólnie kontrakt powinien być minimalny - to co potrzebne "biznesowo". Im więcej dziwnych rzeczy się nawrzuca tym mniej elastyczny i ginie cały zysk z programowania obiektowego (i możliwości dopasowywania implementacji).
Bo jak już wiemy co i w której lini co siedzi - to proszę państwa jest to BASIC.

0

sygnatura

Powie nam najwyżej jak to wywołać i nic więcej, bo type system za wiele nam nie zdradzi. Nie powie nam nic na temat sposobu działania, tego czy coś jest threadsafe itd.

testy

I mówi to ktoś kto jeszcze przed chwilą sugerował żeby opędzać wszystko testem integracyjnym włącznie ze stawianiem serwera http? :D To co mi taki test powie, kiedy mnie interesuje użycie jakieś internalsowej klasy. Z poziomu kodu w teście to nawet nie będę wiedział czy ta klasa jest użyta czy nie. A jeśli nawet ktoś zrobił test klasy która korzysta z tego co mnie interesuje to znów będzie tam tylko jakiś szczególny przypadek na potrzeby testu integracyjnego, pewnie bez określenia warunków brzegowych (sam mówiłeś że happy path cię tylko interesuje). Jakby to było to znienawidzone przez ciebie TDD ze 100% pokrycia to jeszcze by nam coś powiedziało, a tak?

dokumentacja

Widziałem w życiu trochę projektów, nawet takich pisanych zgodnie z Document Driven Development, ale takiej sytuacji żeby internalsy były dobrze udokumentowane to nie widziałem jeszcze. Ba, żeby się taka klasa dorobiła więcej niż jednego zdania javadoca to już by było święto. W praktyce przecież istnieje przeświadczenie (z którym zresztą się zgadzam) że kod powinien się sam dokumentować i być napisany tak jasno że niczego nie trzeba wyjaśniać komentarzami (co najwyżej decyzje "dlaczego tak napisaliśmy" ale nie "co tam sie dzieje").

Możesz zdradzić, jeśli to nie tajemnia, w jakiej dziedzinie (jakieś finanse?) pisze się kod gdzie:

  • sygnatura wszystko zdradza (to chyba nie w javie, bo wtedy trzeba by tylu klas że permgen na 5GB)
  • pisze się pełne pokrycie testami integracyjnymi, żeby dobrze dokumentować kontrakty
  • dokumentuje się wszystkie internalsy, żeby kolega z zespołu nie musiał zaglądać do źródeł
    ?
2
Shalom napisał(a):

sygnatura

Powie nam najwyżej jak to wywołać i nic więcej, bo type system za wiele nam nie zdradzi. Nie powie nam nic na temat sposobu działania, tego czy coś jest threadsafe itd.

Odpowiadam: nie jest threadsafe, chyba, że ktoś w dokumentacji wspomni inaczej.

testy

I mówi to ktoś kto jeszcze przed chwilą sugerował żeby opędzać wszystko testem integracyjnym włącznie ze stawianiem serwera http? :D To co mi taki test powie, kiedy mnie interesuje użycie jakieś internalsowej klasy. Z poziomu kodu w teście to nawet nie będę
wiedział czy ta klasa jest użyta czy nie.

Bardzo dobrze: internalsy nie powinny Cie interesować - dlatego sa internalsami.

Możesz zdradzić, jeśli to nie tajemnia, w jakiej dziedzinie (jakieś finanse?) pisze się kod gdzie:

  • sygnatura wszystko zdradza (to chyba nie w javie, bo wtedy trzeba by tylu klas że permgen na 5GB)
  • pisze się pełne pokrycie testami integracyjnymi, żeby dobrze dokumentować kontrakty
  • dokumentuje się wszystkie internalsy, żeby kolega z zespołu nie musiał zaglądać do źródeł

W finansach (bankach) to tak na pewno nie piszą (nie ma potrzeby) - zresztą prawie nigdzie się tak nie pisze - bo to szkodliwe.

Jak to wygląda w praktyce:

  • sygnatura powinna dużo zdradzać. Zwykle tego nie robi , bo to to nie jest to łatwe, ale da się z tym żyć, (tu się uczymy jako programiści - jest z roku na rok lepiej),
  • konktrakty sa dokumentowane testami przykładowymi - które pokazują co można z metodą zrobić i nic więcej!,
  • internalsów oczywiście się nie dokumentuje, a kolega powinien zaglądać do źródeł tylko jak robi review, (internalsy są internalsami i nie wychodzą z domu - nie wolno im),

I oczywiście , często jest tak ,że czegoś brakuje itp.: wtedy się prosi kolege o uzupełnienie testu, dokumentacji. Czasem zmianę sygnatury (często tylko nazwy...).
Czasem pisze test sam i pytam autora czy to jest opdowiednie użycie (rozpoznanie walką).

Na koniec (teoretycznie) lądujemy z kodem - w którym to co jest potrzebne i stanowi konkrakt, i jest użyte!!!, jest jakoś udokumentowane - a wszystko inne - nie!!! I bardzo dobrze - to znaczy, że można swobodnie zmieniać (wyrzucić!).

W praktyce zawsze tworzy się jakaś szara strefa niedomówień i gdzieś czasem błędy się pojawiają.
Z pewnością nikomu (jeszcze?!) nie przyszło do głowy coś synchronizowac na obiektach z innej metody...
Najczęstszy bład no#1 to jest : "a myślałem, że masz tą zawsze listę posortowaną!" (a przecież nie było nigdzie tak napisane :-)).

( pracuje w ubezpieczeniach - mam w sumie czasu ile chce, na poprawianie, refaktoring i review kodu,
to jest drobny odchył (głównie chodzi o security), ale z drugiej strony nikomu sie aż tak bardzo nie chce poprawiać :-), więc bardzo od zwykłej firmy to nie odbiega).

2
Afish napisał(a):
Wibowit napisał(a):

Był argument o wydajności testów integracyjnych, a więc bajka o dostępie do bazy danych. Tu sprawa jest prosta - można sklecić testową infrastrukturę imitującą dostęp do bazy danych. Podobnie można zrobić imitacje zewnętrznych serwisów. Z czymś takim testy są wystarczająco szybkie.

Nie zgodzę się — właśnie o to chodzi w teście integracyjnym, żeby zintegrować komponenty i sprawdzić, czy one naprawdę razem działają. Już nie raz moja aplikacja przechodziła wszystkie testy, a potem okazywało się, że jednak ORM nie potrafi przetłumaczyć jakiegoś zapytania, mimo że z bazą in memory poradził sobie bez problemu.

Do testów integracyjnych mamy środowiska testowe na których stawiamy kompletny zestaw mikroserwisów z danymi testowymi. Do tego mamy specjalne RESTowe endpointy, które wykonują testowe operacje typu "złóż zamówienie", "sprawdź czy zamówienie X pojawiło się w mikroserwisie Y", etc W ten sposób można efektywnie pokryć dość dużą część funkcjonalności, którą ciężko przetestować bez bazki czy bez mikroserwisów od których zależymy.

Z samymi bazkami sprawa jest dość ciężka w takiej korpo jak u mnie, bo o testowego Oracle'a (w sensie takiego przypisanego do agenta budującego w CI) niespecjalnie łatwo, a niestety mamy zapytania SQLowe, które jego wymagają.

Co do mockowania vs używania rzeczywistych zależności to różnice są takie:

  • mocki wymagają o wiele szerszych zmian przy zmianie sygnatur metod. przykładowo jeśli mamy zmienianą klasę X i od niej zależą klasy A, B i C to w przypadku mockowania musimy poprawić testy dla klas A, B i C (bo w nich mockujemy X'a), natomiast jeśli używamy rzeczywistych zależności to aktualizujemy tylko testową fabrykę produkującą Xa,
  • jeżeli zmieni się kontrakt danej metody czy klasy (nazwijmy ją X) to i tak dalej dla mnie użycie mocków jest gorsze. odpowiedzi mockujemy zawsze używając aktualnego kontraktu danej zależności. jeśli ten kontrakt się zmieni to zostaniemy z zamockowanym starym zachowaniem (nawiązując do poprzedniego punktu - w testach jednostkowych dla A, B i C będziemy mockować X używając jego starego kontraktu). stąd testy jednostkowe klas wykorzystujących zamockowane X będą dalej przechodzić mimo, że wywaliłyby się przy użyciu rzeczywistych zależności.

Pisząc o testach integracyjnych nie mam na myśli używania rzeczywistego I/O. Większość klas używanych powinna działać tylko na danych dostarczanych od klas używających (podejście funkcyjne - pure functions/ referential transparency). Nieliczne klasy zajmujące się I/O powinny być w takich testach integracyjnych zastąpione testowymi implementacjami. Wtedy te testy integracyjne odbywają się bez I/O, czyli są mniej więcej tak samo szybkie jak te oparte na mockach. Nazwa "integracyjne" może być tutaj myląca, ale są one znacznie bliższe tym w pełni integracyjnym niż tym zupełnie jednostkowym.

ps:
przez te wojenki znowu się nie wyśpię :(

3
Shalom napisał(a):

Podoba mi się twój przykład bo pokazuje mniej więcej dylemat który miałem na myśli. Niemniej dla mnie nadal nie jest do końca jasne kiedy traktować coś jako zależność a kiedy jako własność danej klasy, szczególnie kiedy np. ten twój generator jest częścią większego procesu, który jest częścią większego procesu... I nagle okazuje się, że jednostkowo można przetestować tylko sam dół, bo potem im wyżej tym test bardziej puchnie żeby przygotować odpowiedni input.

Zależności to świat zewnętrzny i rzeczy wymagające reużycia. Własności to rzeczy pomocnicze - np. jak przetwarzanie pojedynczego przelewu w pojedynczy rekord pliku, albo przeprowadzenie jakichś innych tylko w danym kontekście obliczeń.

Idąc dalej moim przykładzie, ja nie testuję dołu tylko górę, czyli cały procesor - nie piszę oddzielnych testów jednostkowych dla generatora nagłówka albo procesora pojedynczego przelewu, bo one same w sobie nie dostarczają żadnej mierzalnej wartości biznesowej. Celem jest wyplucie całego, gotowego pliku, nie jego kawałka.

A odpowiedź na puchnący input to użycie frameworka automockującego, który jest w stanie zbudować dowolnie złożony graf obiektów. I to dotyczy każdego typu testów, nie tylko tych "po mojemu". Do którego projektu testowego nie zajrzę, to 70% kodu to jakieś hybrydy builderów z constami, czy inne objectmotherfackery, które są kupą dobrej, nikomu niepotrzebnej roboty. Większość tego da się zastąpić kilkoma linijkami kodu.

  • Czy ktoś nie zapomniał odpalić filtrowania danych - cała reszta niejako z automatu jest sprawdzona przez type system, ale ten jeden krok nie. Jeśli ktoś przypadkiem zakomentuje filtrowanie to trudno to będzie zauważyć.

Jeśli ktoś zapomni odpalić filtrowania, to testy tu w niczym nie pomogą, bo równie dobrze ten ktoś może zakomentować testy i zapomnieć ich odkomentować.

No i tu dochodzimy do szeregu pytań:

  1. Dlaczego nie użyto jedynego pewnego rozwiązania, czyli właśnie systemu typów?
  2. Dlaczego filtrowanie nie jest robione tam, gdzie ma największy sens wydajnościowy, czyli na poziomie źródła danych?
  3. Czemu na review nie wykryto takiej wtopy?
  4. Czemu Sonar nie wykrył zakomentowanego kodu?

I proszę darować sobie bujdy na temat wyczerpującego zestawu danych dla testu integracyjnego, bo taki magiczny zestaw który obejmie wszystkie możliwe przypadki, to jest marzenie ściętej głowy. W tym przypadku to nie problem, ale przy nieco bardziej skomplikowanym kodzie może to być nietrywialne.

Marzeniem ściętej głowy jest też wykrycie wszystkich wszystkich możliwych błędów w testach jednostkowych. Dopiero przy integracyjnych może się okazać, że nie obsłużony jest brak dostępu do udziału sieciowego, nagle odebrane uprawnienia do wykonania procedury w bazie, albo nieudokumentowane różnice w rzeczywistym serwisie zewnętrznym, z którego korzystamy.
Testy oszukasz, życia nie oszukasz.

  • Sytuacje wyjątkowe - szczególnie jeśli obsługa nie jest widoczna wyżej, tzn nie rzucamy nowego wyjątku tylko na przykład coś logujemy, albo generujemy event który wyświetli userowi komunikat błędu itd.

Hmm... eventy z serwisu do UI? Może jeszcze RESTem? ;)

Czym w ogóle różni się obsługa tych różnych typów wyjątków w Twoim abstrakcyjnym przykładzie? Moim zdaniem to w ogóle nie jest miejsce na takie rozróżnianie - po prostu łapiemy i logujemy wyjątek, a w odpowiedzi do GUI wysyłamy kod błędu/jakiś prosty komunikat. Czyli nie Optional.empty(), ale ServiceResult z isError == true.

Przynajmniej do czasu kiedy ktoś zgłosi że funkcjonalność nie działa i nie jest to nijak sygnalizowane, okienek z błędem nie ma, w logach pusto itd ;]

Ten problem występuje chyba tylko jeśli każdy serwis obsługuje błędy sam, i każdy ekran ma swojego errorboxa.

Tam, gdzie ja odpowiadam za architekturę, wszystko to jest generyczne. Serwisy aplikacyjne opakowane w interceptor, w którym jest jedyny try...catchz logowaniem; komunikacja GUI <-> aplikacja też w jednym miejscu; odbieranie info o błędzie i wyświetlanie go także tylko w jednym. W efekcie nie ma tak, że nagle zniknęła obsługa jakichś błędów (o ile ktoś nie zrobi pustego catcha, no ale pomińmy ten temat, bo to forum programistyczne, nie psychiatryczne).

Plus: nie ma ryzyka ze zapomnisz czegoś dodać do mapy bo IoC wrzuci wszystkie implemetnacje dostępne w runtime, nie trzeba modyfikować niczego kiedy dodajemy nową implemetnacje bo będzie dostępna automatycznie.

Nie ma ryzyka, bo jak dostaję taska "dodaj obsługę nowego banku", to pamiętam o tym, że dodać też dla niego test integracyjny. Błąd z mapą szybko wyłapię.

pingwindyktator napisał(a):

Chcąc zamockować obiekty, musimy trzymać je jako interfejs, coby podczas testów wrzucić na ten interfejs ów mocka. No przynajmniej w wielu językach tak jest.

No tak - jeśli chcę mockować, muszę mieć interfejs. Ale ja właśnie nie chcę mockować każdej klasy, a tylko zależności. Klasy pomocnicze niech sobie będą nawet tworzone przez ich "właściciela".

Shalom napisał(a):

Poszczególne BankProcessory jednostkowo ale już cały ExportService tylko integracyjnie, co oznacza że najpewniej w tym ExportService będą nieprzetestowane ścieżki ;]

Nie będzie, bo tam nie ma ścieżek. To tylko szereguje wywołania innych klas w odpowiedniej kolejności. Obsługa wyjątków oddelegowana na zewnątrz.

A czemu nie? Co jest w takim teście złego? Czy jeśli ktoś przypadkiem w trakcie refaktoringu nam naszego error handlera zakomentuje to test się wywali? Wywali się. Więc jak dla mnie spełnia swoją rolę.

Test się wywali, ale tylko raz, bo później już go nie będzie.
Praktyka jest taka, że jak ktoś będzie refaktoryzował kod opakowany testami szczegółów implementacji (takimi jak assertWasCalled), to wyłączy/usunie takie testy.

Test który sprawdzi czy nasz mockowany serwis dostał odpowiedni argument a następnie asercja czy wynik zwrócony przez testowaną metodę jest tym samym co zwrócił serwis jest zupełnie poprawnym i wartościowym testem, bo chroni nas przed tymi 2 potencjalnymi błędami wspomnianymi wyżej.

Tak, tylko to samo zrobi test integracyjny.

Mały Mleczarz napisał(a):
  1. Wyjatek: Jezeli masz gdzies Endpointa i wysylasz cos na zewnetrzny serwis (mail, http, event) fire & forget - to jeszcze taki Mock i testowanie ujdzie.

Do tej listy dodałbym jeszcze np. zwalnianie zasobów (pliku, bazy). Ogólnie rzeczy związane z efektami ubocznymi.

Shalom napisał(a):

No przepraszam, ale jeśli zmieniłeś zachowanie metody a twój test się nie wywalił to taki test jest zupełnie bezużyteczny.

Testy mają się wywalać, gdy metoda zacznie zwracać złe wyniki dla dobrego wejścia. Jeżeli wynik nadal jest dobry, a test się wywala, bo jakaś metoda przestała być wołana gdzieś wewnątrz kodu, to taki test jest po prostu irytującym śmieciem.

To, czy mechanizm cachujący działa, czy do faktycznego źródła danych idzie tylko jeden request, a później reszta jest już brana z cache - to wymaga dodatkowych testów. Ale stare testy powinny przejść, jeżeli nie przechodzą, to źle ten cache wstawiliśmy do aplikacji.

jarekr000000 napisał(a):

Mozecie forkowac i zrobic to na DI framework czy jak uwazacie powinno byc slusznie.

Ale to jest te siedem klas na krzyż? To po co tam jakiś framework?
Frameworki DI się przydają w rzeczywistych projektach, a nie w jakichś abstrakcyjnych przykładach.

Shalom napisał(a):

Możesz zdradzić, jeśli to nie tajemnia, w jakiej dziedzinie (jakieś finanse?)

@Shalom, a Ty w ogóle wiesz z kim tak zaciekle dyskutujesz? Bo mi to wygląda trochę tak, jakbyś chciał ojca uczyć dzieci robić. :P

1

@Shalom, a Ty w ogóle wiesz z kim tak zaciekle dyskutujesz? Bo mi to wygląda trochę tak, jakbyś chciał ojca uczyć dzieci robić. :P
@somekind - ej, chyba nie jestem raczej kimś ważnym(?), (albo mnie coś ominęło jak spałem :-) ). Może i jestem showmanem i posiadam średniej wielkosci brodę, ale to (niestety) nadal nie przekłada się na automatyczne manie racji...

Poza tym (a git raczej nie kłamie) - historia moich commitów pokazuje, że gdyby ta dyskusja toczyła się w 2012, to pewnie zaciekle walczyłbym po stronie kontenerów. Ale cóż - zmieniłem zdanie!

3

a Ty w ogóle wiesz z kim tak zaciekle dyskutujesz? Bo mi to wygląda trochę tak, jakbyś chciał ojca uczyć dzieci robić.

@somekind z całym szacunkiem do @jarekr000000 ale generalnie jestem sceptyczny do przyjmowania na wiarę tego co mówią różne autorytety w branży. Szczególnie że większość z nich prędzej czy później pisze artykuły z serii kilka lat temu byłem gorącym zwolennikiem XYZ, ale okazało się że to się nie sprawdza albo czytam co napisałem kilka lat temu i łapie się za głowę jakie bzdury wygadywałem :) Stąd też próbuje tutaj posłuchać o podejściach innych niż moje i spróbować skonfrontować je z moimi wyobrażeniami i doświadczeniami.

0
somekind napisał(a):
jarekr000000 napisał(a):

Mozecie forkowac i zrobic to na DI framework czy jak uwazacie powinno byc slusznie.

Ale to jest te siedem klas na krzyż? To po co tam jakiś framework?
Frameworki DI się przydają w rzeczywistych projektach, a nie w jakichś abstrakcyjnych przykładach.

Uwazam, ze to jedyne sluszne podejscie - przeduskutowac cos (nawet na 7 klasach - teraz jest tam wiecej: work in progress https://github.com/javaFunAgain/magic_service_story/tree/40_FULL_BAD_VERSION).

Bo, jezeli stawiamy pytanie - "co do wstrzykiwania" itp - to pewnie gdzies tam dyskusja poprowadzi... w maliny.
Natomiast jesli pytanie jest postawione: mam taki konkret do wykonania - server ma robic cos tam etc., to wtedy okaze sie, ze zadne wstrzykiwanie nie jest do niczego potrzebne. (Kiedys (w 2008) moze bylo, jak nie umielismy programowac).

0

Hmm, w takim razie jeżeli DI jest niepotrzebne to dlaczego np. Google tworzy do tego własne biblioteki (np. do Javy i C++) argumentując to tym, że przy pracy przy największym ich projekcie (Adwords) doszli do tego że tego potrzebują? No chyba, że oni po prostu nie umieją programować?

1
tdudzik napisał(a):

Hmm, w takim razie jeżeli DI jest niepotrzebne to dlaczego np. Google tworzy do tego własne biblioteki (np. do Javy i C++) argumentując to tym, że przy pracy przy największym ich projekcie (Adwords) doszli do tego że tego potrzebują? No chyba, że oni po prostu nie umieją programować?

Eeee...

  • Google wymysla mnostwo rzeczy, ktore jakos wcale sie nie przyjmuja (Go i Dart - sa nadal daleko od mainstream) - chociaz sa OK,
  • Google wymysla tez rozne rzeczy, ktore wydaja sie genialne - ale po latach okazuja sie, ze to straszna kicha @see GWT - (na szczescie zdycha),
    (czy tezn Angular1 - ktory wydawal sie genialny -ale ... jednak jest bezsensownie skomplikowany w stosunku do alternatyw i Angular 2),
  • W Googlu jednak pracuja glownie mlodzi ludzie bez doswiadczenia! Moze warto ich czase posluchac, bo czasem maja ciekawe pomysly - to nie warto wszystkiego brac bezmyslnie na produkcje,
  • Nie wiem jak wyglada architektura Adwords - moze rzeczywiscie jest tam Guice czy cos innego do czegos przydatne - ale czy to znaczy, ze Ty masz automatycznie taka sama architecture i potrzeby? A moze Adwords to juz stary project i jest tak zepsuty, ze bez wstrzykiwania sie nie obejdzie....
0

Przy okazji, jeden z glownych problemow kontenerow DI to Magia - dziala czesto, ale nie zawsze (dzis jest Piatek ;-) ). Jak przestaje dzialac to czasem nie wiadomo nawet gdzie szukac (a bo kolega dorzucil gdzies jedna implementacje i kontener zglupial).

Mozna to czesciowo ominac szukajac rozwiazan w kompilatorze (w Scali calkiem dobrze dziala).
A tu sa proby Javowcow przeniesienia jednego patternu do Javy:
http://stackoverflow.com/questions/14248766/cake-pattern-with-java8-possible
http://thoredge.blogspot.ch/2013/01/cake-pattern-in-jdk8-evolve-beyond.html

Cos moze z tego bedzie i magie uda sie wyplenic.

0

@jarekr000000, ale cake pattern, to zło przy większych projektach > http://www.warski.org/blog/2011/04/di-in-scala-cake-pattern-pros-cons/

0

@Koziołek Nie wiem czemu Adam tam sobie robi takie problemy (wyglada na rzucanie sobie klod pod nogi), ale trudno mi tu cos powiedziec . Samemu swiadomie nie uzywam cake pattern (tylko przez przypadek czasem wyjdzie ). A duzych projektow/modulow juz praktycznie nie mam i coraz mniejsza szansa, ze bede mial - (Chedozona) era mikroserwisow!

BTW. Z powodu podróży chwilowo musiałem porzucić wątek, ale na pewno nie na długo.
Jak zwykle okazało sie, że podróże kształcą - z nudów dotarłem do artykułu, który Uncle Bob (zawsze to jakiś autorytet :-) ) już dośc dawno napisał.
https://sites.google.com/site/unclebobconsultingllc/blogs-by-robert-martin/dependency-injection-inversion

1

Uncle Bob (zawsze to jakiś autorytet :-) ) już dośc dawno napisał.
https://sites.google.com/site/unclebobconsultingllc/blogs-by-robert-martin/dependency-injection-inversion

Przeczytałeś to do końca? Gościu uzasadnia swoje pomysły tym, że nie chce mieć kodu frameworka IoC rozsmarowanego po całej aplikacji, a to z kolei tym, żeby mieć w przyszłości możliwość zmiany tego frameworka. No litości. W 2016 roku całe użycie frameworka sprowadza się do skonfigurowania i zarejestrowania go w środowisku i już. Zmiana na inny jest punktowa.
Inną sprawą jest to, że programiści - w tym i ja - poświęcają dużo czasu na pisanie ogólnych rozwiązań, żeby m.in. w razie zmiany którejś z używanych zewnętrznych bibliotek nie trzeba było w kodzie dużo poprawiać. Przy czym z mojej praktyki wynika, że takich zmian NIGDY się nie robi.

1
ŁF napisał(a)

W 2016 roku całe użycie frameworka sprowadza się do skonfigurowania i zarejestrowania go w środowisku i już. Zmiana na inny jest punktowa.

No nie do końca punktowa. Na przykładzie Springa, który jest najpopularniejszym DI dla Javy. Aplikację, korzystającą ze springa można łatwo rozpoznać ponieważ do obsługi mechanizmu DI używa ona własnej adnotacji @Component. Bez niej Spring jest bezbronny/musimy korzystać z XMLa. Jednocześnie istnieje standardowy pakiet javax.inject, który powinien wystarczyć do skonfigurowania aplikacji i podobno Spring implementuje obsługę tego pakietu... tyle tylko, że bez sterty xmli i własnych adnotacji nie potrafi odszukać komponentów. Zatem nie ma możliwości punktowej zmiany kontenera DI, ponieważ Spring mocno ingeruje w kod aplikacji.

0

@Koziołek gadasz bzdury. Od dawna Spring wspiera @Named oraz inne standardowe adnotacje javy z javax.inject. Pracuje teraz z projektami z użyciem Spring IoC i w zasadzie nigdzie nie ma odwołania do Springa - zero ingerencji w biznesowy kod.
Dodatkowo jeśli ktoś uzywa Spring Boota to cała ta wielka konfiguracja to jest jedna klasa na kilka linijek. A jeśli ktos tworzy kontener ręcznie np. w aplikacji standalone (bo chce potem to móc zmienic na Guice czy Welda) to ilość konfiguracji jest zupełnie porównywalna z tą dla Guice i Welda, i znów mieści się spokojnie w jednej klasie na kilkadziesiąt linijek.
Kilka lat temu faktycznie tak było że projekty ze Springiem były mocno związane, ale głownie dlatego że nie istaniał jeszcze javowy standard opisujący CDI, toteż każda taka technologia musiała wprowadzić swoje własne mechanizmy.

Trochę inaczej ma sie kwestia np. użycia Spring MVC, no ale znów nie ma tu standardu javy...

0

@Shalom, ale ja mówię o czymś trochę innym. Spring potrafi obsłużyć adnotacje z javax.inject, ale by klasa była traktowana jako zarządzana przez kontener DI nadal musisz użyć @Component na tej klasie (albo xmla). Inaczej Spring nie ogarnia, nawet jeżeli pole, które wstrzykujesz ma typ konkretnej klasy.

0

A a ci mówie że od dawna nie trzeba bo adnotacje z CDI też są obsługiwane i @Component z powodzeniem zastępuje @Named tak samo jak @Autowired zastępuje @Inject :)

2

Bawiąc się dalej na githubie - doszedłem do tego, że MagicService powinien był wygladać mniej więcej tak:

 
 public class MagicService {
     private final DataProducer dataProducer;

     private final DataProcessor dataProcessor;

     private final OutputFormatter outputFormatter = new OutputFormatter();

     public MagicService(DataProducer dataProducer, DataProcessor dataProcessor) {
         this.dataProducer = dataProducer;
         this.dataProcessor = dataProcessor;
     }

     public Either<CalculationProblem, Output> performComplexCalculations(Input input){
             final Either<InputProblem, AccessibleDataFormat> inputData = dataProducer.extractData(input);
             final Either<CalculationProblem, GeneratedResult> generatedData =
                     inputData
                             .mapLeft( prob ->new CalculationProblem(prob))
                             .flatMap( data -> dataProcessor.process(data).mapLeft(
                                     prob -> new CalculationProblem(prob)
                             ));

             return generatedData.map(outputFormatter::formatOutput);
     }
 }

A jak nie ma tylu zależności - to konstruktor jak najbardziej do zależności wystarcza. A o ile się nie używa bzdurnych frameworków typu Spring lub JEE - to łatwo utrzymać design w takich ryzach. I testy są proste (bez Mocków)....

A jak do tego doszło (od początkowego zagadnienia jest opisane tu)
Odcinek 4 https://github.com/javaFunAgain/magic_service_story/tree/40_FULL_BAD_VERSION
Odcinek 5 https://github.com/javaFunAgain/magic_service_story/tree/50_LETS_CHANGE
Odcinek 6 https://github.com/javaFunAgain/magic_service_story/tree/60_REFACTOR_IT

Przy okazji - to nie koniec.

3

W poprzednich postach i na moim githubie pokazałem jak można obejść się bez frameworku DI w przykładowym problemie postawionym przez @Shalom. Ta praca, się jeszcze nie skończyła – bo zamierzam ten zabawny projekt pociągnąć dalej.

Myślę jednak, że w zasadzie mogę odpowiedzieć na postawione zagadnienie - DI , Testy itp.

Tylko uwaga długie :-(

Na wstępie:

Jestem silnym przeciwnikiem frameworków DI - typu Spring, Guice lub JavaEE (olewając fakt, że JavaEE to nie miał być framework DI :P ).
Uważam, że injectiony (@Autowired, @Inject, @ejb czy niejawne) to takie GOTO naszych czasów i trzeba to zwalczać.

Dlaczego:

  1. Niepotrzebne/ bez sensu
    Po pierwsze w większości przypadków nie są potrzebne. Jest to zbędna "magia" dorzucana do kodu, która powoduje, że nawet jeśli kod się kompiluje i przechodzi wszystkie unit testy ... to nadal może nie działać (bo np. ktoś coś zmienił w konfigu - wstrzyknięcia nie są jednoznaczne etc.).

Jeżeli jakaś klasa, którą obrażę - nazywając ją czasowo Beanem, ma tylko jedną implementację w systemie - wówczas z pewnością żadne wstrzykiwanie nie jest potrzebne - bo przecież wiadomo co tworzymy i można użyć 'new'... (Pomijam tutaj testowe instancje - ten problem ogarnę osobno).

1.1 Zarażeni
I tu podnosi się krzyk, a co jak ta instancjonowana klasa ma jakieś Injectiony! Przecież, po użyciu 'new', nie zainstancjonują się!
Dokładnie, nie zainstancjonują się - i tu widać pierwszą dużą wadę frameworków DI - są jak wirus HIV - jak jeden Bean się zarazi, to wszystkie współpracujące automatycznie
muszą ulec zarażeniu i system w sposób spektakularny zmienia się w wielką wylęgarnię HIV. (A przecież, jak wszyscy będą zarażeni to już problemu nie będzie...).
(Ten akapit nie ma na celu dyskryminować nikogo z HIV – potrzebowałem, jako przykład, wybrać znaną chorobę, która roznosi się między innymi przez stosunki płciowe).

1.2 Multum zależności
No, a co z klasami, gdzie jest 5-10 zależności - będziesz to wszystko instancjonował ręcznie? Otóż odpowiedź jest prosta -
takie klasy to właśnie objaw zarażenia DI HIV-em. W normalnych warunkach po dwóch, trzech zależnościach zapala się czerwona lampka i programista przemyśliwuje design (no dobra - tak jeszcze nie jest, ale się poprawia).
Mając framework DI ... można sprawę olać - coś tam się wstrzyknie i będzie działać! To jest super ! I jeśli walczymy o szybki prototyp takie podejście może się sprawdzić.

Natomiast próba dłuższego utrzymania takiego kodu to niestety koszmar. Testowanie, choćby nie wiem jak sprytne były Mocki,jest albo nieefektywne (testujemy Mockito), albo bardzo żmudne.
Najgorsze, że jak się odpowiednio w takie bagno wdepnie - to powstaje system opleciony gigantyczną siecią zależności miedzy klasami, których każde zerwanie (refaktoring) boli.

Jako, że mam na utrzymaniu kilka systemów napisanych w JavaEE oraz Springu - widzę ile czasu spędzam z zespołem na odkrywaniu dlaczego coś mi się tu nie wstrzyknęło (zwykle jest tak po każdym refaktoringu). Czy muszę pisać, że Mocki dla odmiany wstrzykują się świetnie i testy przechodzą bez problemu....

1.3 Inicjalizacja/ Startup
No, a co z inicjalizacją systemu - taki framework DI potrafi bardzo ładnie poukładać kolejność startowania elementów i nie trzeba się przejmować, całą tą siatką połączeń.
W zasadzie jest to ciekawy argument, oznacza, że pogodzenie się, z faktem nie panowania nad systemem....

Z drugiej strony jako „anarchitekt”, paru systemów przyznaje, że akurat totalnie panowanie nad systemem nie jest tak ważne - ważne jest aby system działał!
I tu po pierwsze: są lepsze rozwiązania - jak np. lazy val w Scali. W przypadku Javy - mamy różne patterny, które to robią (http://www.theserverside.com/news/1321145/Programmatic-Dependency-Injection-with-an-Abstract-Factory – choć akurat to taki sobie artykuł).

Prawdziwy problem frameworków DI to niestety: nieprzewidywalne zachowanie. Wystarczy jedno przepakietowanie i może okazać się, że połowa systemu nie wstaje.
Bo jest zupełnie inna kolejność inicjalizacji!

Posiadanie systemu, który wstaje w magiczny sposób wcale nie jest dobrym rozwiązaniem na dłuższą metę.
Dorobiłem się już nawet jednego systemu, który zawsze źle wstaje (z Exceptionami)... - i nikt juz nie wie jak go rozplątać (tu akurat zrobił swoje OSGi).

1.4 Magia
To jest w sumie najważniejszy argument. Jak mówił Greg Young - "try to explain dynamic proxy to junior". Istniejące frameworki, ze względów technicznych mają czasem zabawne ograniczenia. Piękne katastrofy dzieją się jak się napisze - return this; albo odwoła do metody z tego samego beana:
this.callAnotherMethodThatNeedsSomeInterceptorLikeForInstanceTransactionHandling
... i nagle nie działają interceptory. Ja wiem, jak takie frameworki działają i generalnie nie mam z tym problemów (oprócz śmiechu, przez łzy ile razy tworze sztucznego beana, bo to żeby sztucznie przedelegowąć wywołanie metody do innego, i interceptory miały szansę).
Ale TO JUŻ NIE JEST JAVA... to jest java z magią, bo zwykłej Javie return this nie jest niebezpieczny!
I naprawdę, nie tylko juniorzy wpadają w takie pułapki, to jest po prostu nieintuicyjne, więc raz na jakiś czas każdy wpada. (Np. po refaktoringu).

1.5 A może jednak

To kiedy warto używać frameworka DI ?
W zasadzie jest jeden rozsądny przypadek - nasz system wspiera pewną koncepcje pluginów/wtyczek i chcemy sobie na etapie budowania systemu, albo nawet runtime
dynamicznie podmieniać implementacje. Jedna implementacja dla klienta A, inna dla B. Wówczas taki framework jest niesamowicie wygodny ... o ile tylko ograniczy się jego zasięg rażenia. Kilka, kilkanaście klas funkcjonujących jako beany - i jest super!
Kiedy ostatnio taki system widziałem... (w dobie mikroserwisów)... już nie pamiętam.

  1. Testy
    Testy - to jest najbardziej tajemniczy przypadek. Nie wiem jakim cudem, ale szczególnie Springowi udało się sprzedać, jako idealny framework do testów. Wystarczy, na czas testów zrobic cały testowy kontekst, z testową bazą danych, innymi implementacjami niektórych beanów, a całe środowisko, wszystko się samo wstrzyknie i życie (testy) będzie piękne.
    2.1 Oh really?
    Pytanie - numer 1, kto korzystając ze springa faktycznie tak robi? Jeszcze parę lat temu to się zdarzało, ale obecnie większość przypadków to Unit testy - i mockito!
    A do tego, żaden testowy kontekst nie jest potrzebny. Co więc przynosi taka praktyka ? - testy które testują Mockito!
    Dlatego, że podnoszenie na czas testów kontekstu Springowegom czy JEE jest w istocie męczące, więc omija się problem i wrzuca wszędzie Mockito. (I unit testy działają bez Springa....).

2.2 Mockito
Najgorsze, że pozór wydaje się, że Mockito to doskonała oszczędność czasu - wystarczy napisać Mockito.when(...) coś tam, coś tam i mock gotowy. To, co jednak w takim podejściu umyka,
to fakt, że mnóstwo takich 'whenów' się powtarza... potem powstaje system, gdzie po każdym refaktoringu trzeba przeryć połowę testów. A co komu po testach, które trzeba od razu zmieniać
z implementacją - w czym one pomagają przy refaktoringu?

2.3 Jak więc?
Jakie jest prawidłowe rozwiązanie -> otóż:

  • odpalanie unitu bez mocków, dokładnie tak jak działa (i wtedy testuje się naprawdę to co działa)!,
  • ręczne pisanie mocków stubów, które potem można wielokrotnie używać,

W zasadzie jeżeli pisze się jakiś większy, cięższy serwis, to warto od razu implementować jego "Mock" (to poniekąd cześć dokumentacji), tak aby innym się łatwo testowało. ( Choć, w praktyce, rzadko to robię...).
Jeśli natomiast jakaś klasa: szybko się startuje, nie zależy od zewnętrznych zasobów... to niby dlaczego utrudniać sobie życie i ją mokować?

2.4 A może ….
I znowu jest przypadek kiedy Mockito jest jak najbardziej słuszne - kiedy odwołujemy się do zasobów systemowych, lub kiedy rozmawiamy z serwisami zewnętrznymi, które ktoś inny napisał.
Wówczas możemy się uniezależnić od ich implementacji i spokojnie posymulować ich odpowiedzi Mockitem. Ale to jest zwykle kilka, kilkanaście takich na aplikację.

Natomiast używanie Mockito, do własnych klas ... to najczęściej choroba -> HIV.

2.5 Testy integracyjne
Ciekawa sprawa te tzw. testy integracyjne, osobiście uważam, że to pojęcie w zasadzie sztuczne. W projektach Spring, JavaEE, OSGI szczególnie to widać.
Testy integracjne to u typowego Javowego zespołu - testy komponentów na kontenerze, gdzie można sprawdzić czy wszystko się po-wstrzykiwało.....
Czyli ich jedynym sensem istnienia, jet fakt, że używany framework DI jest niegodny zaufania i nie można polegać na Unit testach, bo wszystko może rypnąć przy realnym starcie.
Brawo Spring! Naprawdę ułatwia... (przy okazji artykuł (lekko poza tematem): http://blog.thecodewhisperer.com/permalink/integrated-tests-are-a-scam)

  1. Inne nałogi
    Jakie jeszcze inne choroby przynoszą frameworki DI -
    Chyba najgorszy problem to tzw. ContextBean. W wielu dużych projektach, zespoły dorabiają się patternu, gdzie prawie każdy komponent ma wstrzyknięte coś co nazywa się
    ContextBean - i jest tam: konfiguracja systemu, imię, nazwisko i numer buta użytkownika, prognoza pogody na następne pięć minut etc..
    Najfajniej jest jak ktoś tam jeszcze wrzuci HashMapę, która zawierać może wszystko... Jest to problem spowodowany specyficznym czasem życia Beanów np. springowych, które zupełnie nie pasują do czasu cyklu życia funkcji. Więc programiści głupieją, zamiast np. tworzyć sobie Parameter Object , Buildera czy tym podobne.

  2. To nie wina frameworku
    Ale przecież! - Te patologie powyżej świadczą tylko, że zespół jest kiepski, a nie framework.
    To jest argument typu - komunizm był dobry , tylko ludzie za kiepscy. Zupełnie podobne pojawiały się jak Dijkstra pisał "GOTO considered harmful".
    Pewnie! Jak zespół bedzie trzymał dyscyplinę i pilnował każdej linijki kodu - to problemu nie będzie.
    Praktyka, jest jednak taka, że jeżeli środowisko pracy jest jak pole minowe i trzeba dużo czasu spędzać na "patrolowanie" (review), to jest to środowisko do bani.

Lepsze byłoby środowisko, w którym pisanie utrzymywanego (pewnego! )kodu jest promowane! Będzie wychodziło - niejako przypadkiem, bo robienie skrótów będzie od razu boleć.

  1. Po co się mieszam?
    A teraz ode mnie - dlaczego mi tak zależy?
    Jakby nie patrzeć zarabiam na życie zwalczając problemy JavaEE, Spring itp. , bo przeczytałem kiedyś te specyfikacje, znam dobrze javę, proxy, classloadery, jvm...
    Spoko.
    Problem polega na tym, że naprawdę mam już wymioty jak widzę, jak kolejny zespół Javowy tworzy potwora na miarę: "EnterpriseFizzBuzz" (https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpriseEdition), z którego jest naprawdę dumny.
    Kilkaset klas, siedem warstw, osiem frameworków i realizuje funkcjonalność, którą dwa chłopaki od NodeJS robią w 8 godzin (z przerwą na grę w CS)...
    Bo tamci podchodzą do zagadnienia tak:
  • co jest do zrobienia,
  • jak to najprościej - rozsądnie zrobić,
  • robimy.
    Typowy zespół javowy: nie wie jeszcze co będzie robić, nie ma nawet projektu - a już się kłóci Spring czy JavaEE, a może Guice, JPA czy Hibernate, Oracle czy MySQL..., a potem stara się wszędzie powciskać te technologie (żeby udowodnić, że technologie działają i wybór był słuszny).

Co by tu jeszcze wstrzyknąć?....

3
jarekr000000 napisał(a):

W poprzednich postach i na moim githubie pokazałem jak można obejść się bez frameworku DI w przykładowym problemie postawionym przez @Shalom. Ta praca, się jeszcze nie skończyła – bo zamierzam ten zabawny projekt pociągnąć dalej.

No to prawdziwa kupa dobrej, nikomu niepotrzebnej roboty. ;)

Temat nie jest o kontenerach, tylko o testach i zależnościach. Czy zależności wstrzykujemy z kontenera czy sami, to inna kwestia.

Z Twojego posta zrozumiałem tyle, że kontenery DI w świecie Javy są do kitu. Trochę mnie to nawet dziwi - a trochę nie, to w końcu Java i trzeba trzymać kompatybilność wsteczną z plikiem xml napisanym 80 lat temu.

Zaśmiecanie kodu biznesowego jakimikolwiek atrybutami frameworka IoC (albo jakiegokolwiek innego) to jakaś totalna patologia. Podobnie jak pisanie ogromnych XMLi w celu skonfigurowania takiego. (Ale z dwojga złego już XML lepszy, bo nie zaśmieca ani nie wiąże na sztywno biznesu z infrastrukturą.) Nie wiem po co ludzie tak utrudniają sobie życie, ale to ich sprawa. W praktyce nie trzeba robić ani jednego ani drugiego - normalne kontenery DI wspierają konwencje i hurtowe wstrzykiwanie wszystkich zależności w całych modułach aplikacji. To 10-20 linijek kodu na całą aplikację, niezależnie od jej wielkości. To nie jest problem. Kontenery to tylko narzędzia - jeśli ktoś używa słabych narzędzi, na dodatek w nieodpowiedni sposób, to nie znaczy, że narzędzia są złe.

Odrębnym problemem jest mnożenie zależności na siłę. Jeśli ktoś myśli, że każda klasa jest zależnością, każda musi mieć swój interfejs, i każda musi być wstrzykiwana, to najprawdopodobniej popełnia poważny błąd koncepcyjny.
Ale znowu - to nie problem, bo sensowny kontener pozwoli mu na to. Po prostu nie będzie miał żadnej wartości dodanej z tworzenia całego tego kodu.

Wiadomo - lepiej jak klasa ma mniej zależności, ale niektóre klasy na wysokim poziomie w hierarchii programu, wszelkie kontrolery, handlery i inne orkiestratory, mogą mieć tych zależności nawet kilkanaście. I ten problem znowu jest rozwiązywany przez odpowiednio skonfigurowany kontener.

Co do utrzymywalności kodu - kontenery pozwalają nie musieć zmieniać kodu tworzącego obiekty, gdy zmieniamy zależności obiektów (sygnatury konstruktorów), a więc ją zwiększają. To jest cel, i to się da osiągnąć, wystarczy używać sensownych narzędzi, a nie takich, które się konfiguruje w XML, bo XMLa żadnego nie da się utrzymywać.

Co do zależności w testach - owszem, mockowanie to kupa roboty, czasami ponad połowa kodu testowego.
Ale każdy ma wybór, czy tworzy kupę, czy kod. (No dobra, kupa to nie zawsze jest kwestia wyboru.) Ale od tego są biblioteki automockujące, żeby cały SUT wygenerować jedną linjką (albo nawet wstrzyknąć jako parametr do metody testowej), a mockować tylko te metody, które są potrzebne w tym akurat teście. Dzięki temu nawet po zmianie konstruktorów w testowanych klasach, nie posypią się testy (co miałoby miejsce w przypadku klasycznego mockowania). Pisanie ręcznych mocków to strata czasu i trzeba tego unikać - raz pisząc kod, który da się testować bez mockowania, dwa - tam gdzie trzeba mocków, używać automockowania.

Może zamiast kierować swoją nienawiść na kontenery, skieruj ją na Javę? ;)

1

Odrębnym problemem jest mnożenie zależności na siłę. Jeśli ktoś myśli, że każda klasa jest zależnością, każda musi mieć swój interfejs, i każda musi być wstrzykiwana, to najprawdopodobniej popełnia poważny błąd koncepcyjny.
Ale znowu - to nie problem, bo sensowny kontener pozwoli mu na to. Po prostu nie będzie miał żadnej wartości dodanej z tworzenia całego tego kodu.

Wiadomo - lepiej jak klasa ma mniej zależności, ale niektóre klasy na wysokim poziomie w hierarchii programu, wszelkie kontrolery, handlery i inne orkiestratory, mogą mieć tych zależności nawet kilkanaście. I ten problem znowu jest rozwiązywany przez odpowiednio skonfigurowany kontener.

Dyscyplina się nie skaluje. Jak zauważył @jarekr000000 kontener DI ułatwia mnożenie zależności. Prawa Murphy'ego działają - jeżeli istnieje nieznikoma szansa, że zaplątamy się w sieć zależności to to się stanie.

Co do utrzymywalności kodu - kontenery pozwalają nie musieć zmieniać kodu tworzącego obiekty, gdy zmieniamy zależności obiektów (sygnatury konstruktorów), a więc ją zwiększają.

To prędzej sugeruje zaplątanie się w sieć zależności.

Może zamiast kierować swoją nienawiść na kontenery, skieruj ją na Javę? ;)

Ojejciu, ale żeś pojechał. Może pokażesz przewagę tego wspaniałego C# nad Javą?

Pisanie ręcznych mocków to strata czasu i trzeba tego unikać - raz pisząc kod, który da się testować bez mockowania, dwa - tam gdzie trzeba mocków, używać automockowania.

To automockowanie to coś a'la http://site.mockito.org/mockito/docs/current/org/mockito/Mockito.html#RETURNS_DEEP_STUBS ?

Nie widzę specjalnie jak C# ułatwia przekazywanie zależności porównując do Javy. W Javce możemy mieć coś takiego:

class BicycleRepairMan {
  private final GoldenHammerStorage hammerStorage;

  public BicycleRepairMan(GoldenHammerStorage hammerStorage) {
    this.hammerStorage = hammerStorage;
  }

  public void repairBicycle(Bicycle bicycle) {
    GolderHammer hammer = hammerStorage.lease();
    hammer.whack(bicycle);
    hammerStorage.release(hammer);
  }
}

W C# pewnie będzie coś bardzo podobnego.

Z użyciem Lomboka w Javie możemy skrócić kod do:

@AllArgsConstructor
class BicycleRepairMan {
  private final GoldenHammerStorage hammerStorage;

  public void repairBicycle(Bicycle bicycle) {
    GolderHammer hammer = hammerStorage.lease();
    hammer.whack(bicycle);
    hammerStorage.release(hammer);
  }
}

W Scali nie trzeba używać Lomboka, bo składnia udostępnia coś takiego jak główny konstruktor:

class BicycleRepairMan(hammerStorage: GoldenHammerStorage) {
  def repairBicycle(Bicycle bicycle): Unit = {
    val hammer = hammerStorage.lease()
    hammer.whack(bicycle)
    hammerStorage.release(hammer)
  }
}

Ponadto w Scali są takie bajery jak argumenty implicit (w Javie wszystkie argumenty są explicit) czy miksowanie traitów, które mogą zawierać stan (interfejsy w Javie nie mogą zawierać stanu) co razem daje duże możliwości na stworzenie zakręconej hierarchii fixtures (zamiast modułów w kontenerach DI) i "wstrzykiwania" parametrów. Zaletą używania argumentów implicit i miksowania traitów nad używaniem kontenera DI jest to, że argumenty implicit i traity to podstawowe elementy języka i są w pełni wspierane przez IDE i kompilator. Elementy są wrzucane w konstruktory na etapie kompilacji (a więc kompilacja się sypnie jak nie będzie żadnego argumentu implicit w zasięgu do dobrania przez kompilator), a sam kod poddaje się dobrze statycznej analizie, więc mamy wszelkie udogodnienia od IDE jak nawigacja po kodzie (skąd się co bierze - IntelliJ pokazuje lokalizację argumentów implicit), itd

Update:
Kod obrazujący Scalowe triki:

object Testing {
  implicit class TestDescription(value: String) {
    def in(body: => Unit) =
      body
  }

  def main(args: Array[String]): Unit = {
    "mixing in dependencies to fixture works" in new Fixture {
      println(method1(dep1))
    }

    "mixing in dependencies inline works" in {
      val setup = new DepSrc2 with DepSrc3
      import setup._ // tutaj wciągamy sobie pola ze stałej setup w zasięg
      val dep1 = new Dep1
      println(method1(dep1)) // reszta parametrów jest brana z setup
      // bo je zaimportowaliśmy
    }
  }

  class Dep1
  class Dep2
  class Dep3

  trait DepSrc1 {
    implicit val dep1: Dep1 = new Dep1
  }

  trait DepSrc2 {
    implicit val dep2: Dep2 = new Dep2
  }

  trait DepSrc3 {
    implicit val dep3: Dep3 = new Dep3
  }

  trait Fixture extends DepSrc1 with DepSrc2 with DepSrc3

  def method1(dep1: Dep1)(implicit dep2: Dep2, dep3: Dep3): Int =
    method2(dep1, dep2) /* argumenty zawsze można podać explicit */ + method3

  def method2(implicit dep1: Dep1, dep2: Dep2): Int =
    method4 // tu argument leci implicit

  def method3(implicit dep3: Dep3): Int =
    3

  def method4(implicit dep1: Dep1): Int =
    8
}
0
somekind napisał(a):
jarekr000000 napisał(a):

W poprzednich postach i na moim githubie pokazałem jak można obejść się bez frameworku DI w przykładowym problemie postawionym przez @Shalom. Ta praca, się jeszcze nie skończyła – bo zamierzam ten zabawny projekt pociągnąć dalej.

No to prawdziwa kupa dobrej, nikomu niepotrzebnej roboty. ;)

Dzięki za docenienie, staram się :-). Wiele z tego co się nauczyłem zawdzięczam innym ludziom, którzy robili taką niepotrzebną robotę - staram się to powoli odpracowywać (w końcu mam czas).

Co do zależności w testach - owszem, mockowanie to kupa roboty, czasami ponad połowa kodu testowego.
Ale każdy ma wybór, czy tworzy kupę, czy kod. (No dobra, kupa to nie zawsze jest kwestia wyboru.) Ale od tego są biblioteki automockujące, żeby cały SUT wygenerować jedną linjką (albo nawet wstrzyknąć jako parametr do metody testowej), a mockować tylko te metody, które są potrzebne w tym akurat teście. Dzięki temu nawet po zmianie konstruktorów w testowanych klasach, nie posypią się testy (co miałoby miejsce w przypadku klasycznego mockowania). Pisanie ręcznych mocków to strata czasu i trzeba tego unikać - raz pisząc kod, który da się testować bez mockowania, dwa - tam gdzie trzeba mocków, używać automockowania.

Nie wiem co to za bilblioteki automockujące - wpisałem sobie, żeby się temu przyjrzeć (biblioteki C# już nieraz mnie zaskoczyły pomysłami), ale może podasz pomocny konkret?
Bo, w sumie jak są takie automoki cudowne - to ja w sumie zrobił bym tą jedną linijką Automoka i puścił na produkcję. Skoro jest na tyle dobry żeby zastąpić mockowaną klasę w testach - to powinien być też dobry na produkcję (albo raczej się nie rozumiemy - bo naprawdę nie wyobrażasz sobie co ludzie potrafią zamockować (niepotrzebnie)).

(Btw: w javie zaimplementowałem kiedyś mały serwisik na Mockach - ale to było po piwie, miałem też pomysł, żeby to pociągnąć dalej i testy dla odmiany zrobić na konkretnych klasach (ale skończyło mi się piwo)).

Może zamiast kierować swoją nienawiść na kontenery, skieruj ją na Javę? ;)

Trochę masz racji, ale ja uważam, że Java nie jet aż tak zła - jak złe jest jej "enerprisey" community. Goście utkwili w roku 2006 i nie potrafią się z niego wydostać...
Widziałem już projekty w Scali z podciągniętym DI (guice), robione przez byłych javowów - i była to nadal ta sama kupa(normalnie ScavaEE). (Widziałem też w miarę rozsądne użycie Guice (DI) w Playu , (ale i tak nadal nie rozumiem po co)).

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