Testy jednostkowe a metody prywatne

0

Cześć, zawsze czytałem i uważałem, że powinno się jak najmniej używać w programowaniu słowa public. Sam tworząc klasy robiłem zazwyczaj 1-2 metody publiczne, które faktycznie były używane na zewnątrz, a reszta leciała jako private. Starałem się nawet stosować bardziej rystrykcjne scope'y do klas, a nie tylko metod. Wydawało mi się to świetnym rozwiązaniem, aż nie zacząłem pisać do tego testów.
Przykładowo, klasa ma za zadanie na podstawie otrzymanych parametrów stworzyć zapytanie do bazy danych i je zwrocić. Parametrów może być od 1 do 10 i zapytanie się odpowiednio komplikuje, tak więc zrobiłem klasę która wystawiała dokładnie jedną metodę

public Query getQuery(CriteriaDTO dto) {
    return prepareQuery(dto); 
}

gdzie prepareQuery() to prywana metoda, która woła kolejne prywatne metody, ciągnące się na pozostałe kilkadziesiąt linijek danej klasy. Dodatkowo te metody wołają zewnętrzne klasy.

No i pojawił się problem, jak napisać testy do takiej klasy?
Powermockito i mockowanie prywatnych metod? Niby nie powinno się tego nadużywać.
Testowanie samego API czyli metody getQuery()?
Czy może jednak upublicznienie niektórych metod?

0

Skupiasz się na testach i sposobie pisania testów do istniejącego kodu, ale ja tu widzę inny problem.

gdzie prepareQuery() to prywana metoda, która woła kolejne prywatne metody,
ciągnące się na pozostałe kilkadziesiąt linijek danej klasy.
Dodatkowo te metody wołają zewnętrzne klasy.

Samo to (woła kolejne prywatne metody oraz metody wołają zewnętrzne klasy.) wydaje się być problemem (bo wyobrażam sobie to jak jakiś przeinżynierowany, pełen przekierowań kod)

Dodajmy do tego fakt, że na takim etapie zaawansowania klasy nawet nie zacząłeś pisać testów. A w sumie i tak niewiele masz do stracenia. Jeśli napisałeś rozbudowaną klasę z wieloma funkcjami, to pewnie i tak poświęciłeś dużo czasu na manualne sprawdzanie "czy działa". Więc równie dobrze mógłbyś zacząć pisać testy na wcześniejszym etapie, byś miał ułatwiony proces.

Wydawało mi się to świetnym rozwiązaniem, aż nie zacząłem pisać do tego testów.

Jak dla mnie cały proces jest zwalony. Dużo prościej byłoby zastosować TDD, wtedy mógłbyś zacząć pisania od tych swoich "1-2 metod publicznych", które miałbyś otestowane i miałbyś cały czas wszystko pokryte testami, a potem w miarę postępu prac mógłbyś wydzielać kolejne metody prywatne*, do których nie musiałbyś wcale pisać żadnych testów (w sensie - miałbyś ~100% pokrycia testami bez pisania testów. Duże pokrycie kodu testami nie oznacza wcale, że do każdej metody trzeba pisać oddzielny test, bo jak robisz refaktor i wydzielasz prywatną metodę to ona otestuje się sama. Wystarczy, że testy dalej będą przechodzić po refaktorze).

http://butunclebob.com/ArticleS.UncleBob.TheThreeRulesOfTdd

* to jest cudne w TDD. Że możesz zacząć od czegoś prostego, np. pisać kod, w którym wszystko będzie działo się w 1 metodzie i będzie pisane "na pałę", a w miarę postępów możesz refaktorować i wydzielać osobne metody.

2

Piszesz testy tak, jakby wszystko z metody prywatnej było po prostu w metodzie publicznej (wywoływanej w teście).

5

Nie widzę tu żadnego sensu na jakieś mockito czy mocki ogólnie.

Testy do tego robi się prosto:

  1. stawiasz testową baze danych (np. h2)
  2. Przygotowyjesz to CriteriaDTO
  3. Odpalasz public Query getQuery(CriteriaDTO dto)
  4. Tak zwrócone Query odpalasz na testowej bazie danych
  5. Sprawdzasz czy wynik zgodny z oczekiwanym.
4

Skoro coś jest prywatne, to z punktu widzenia zewnętrznego konsumenta (a czymś takim są dla mnie testy) nie wymaga skupiania na sobie uwagi. Tak jak koledzy wyżej piszą - gdybyś zaczął testy od publicznych funkcjonalności a potem wydzielał prywatne funkcje to nie byłoby to bardziej naturalne. Teraz dopisując testy masz wrażenie, że każda funkcja wymaga testowania a to nieprawda. Testuj zachowanie i zwracane rezultaty przy określonych parametrach, zamiast testowania wszystkiego co masz w środku. Z zewnątrz nikt o tym nie wie - konsument oczekuje tylko poprawnego działania danego kawałka kodu.

3

Dodatkowo posiadanie kilku metod publicznych, a cała reszta z innym modyfikatorem dostępu ma dużo sensu. Testujesz system tylko przez punkt wejścia - każdy przypadek jesteś przecież w stanie przetestować właśnie przez punkt wejścia. Ma to dużo plusów, bo jeżeli zmieniasz implementację/robisz refaktor to nie musisz zmieniać i implementacji i testu jeżeli zachowanie się nie zmienia. Tak jak napisał Jarek - jeżeli chcesz przetestować to czy to bangla z bazą to zrób test integracyjny i gdzieś tam jednostkowy testujący getQuery

0
hcubyc napisał(a):

z bazą to zrób test integracyjny i gdzieś tam jednostkowy testujący getQuery

Co właściwie masz na myśli i jak to miałoby wyglądać? Te 2 typy testów w tym przypadku.

Dla mnie test z własną baza danych to nie jest test integracyjny.
Nie jest to też jednostkowy - choć do tego jest najblizej.

To co proponuje to jest to: tadam.... Test.

0

Dobry test to taki (między innymi), który testuje skutki, a nie implementację, zatem nie powinno go interesować co się dzieje wewnątrz klasy tylko to co zwraca. No, chyba że wchodzą w grę odwołania do systemów zewnętrznych: baz danych, REST, itp. Wtedy wypada sprawdzić, czy są rzeczywiście poprawnie odpytywane. Jednak z tego co widzę to nie w tym przypadku.

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

z bazą to zrób test integracyjny i gdzieś tam jednostkowy testujący getQuery

Co właściwie masz na myśli i jak to miałoby wyglądać? Te 2 typy testów w tym przypadku.

Dla mnie test z własną baza danych to nie jest test integracyjny.
Nie jest to też jednostkowy - choć do tego jest najblizej.

To co proponuje to jest to: tadam.... Test.

OK przyznam, że wg definicji nie będzie to typowy test integracyjny, dla mnie to skrót myślowy od testu, gdzie np. trzeba podnieśc cały kontekst aplikacji, dotyka I/Osów etc

3

Z tą definicją testu integracyjnego, to jest tak, że nie wiadomo gdzie jest granica. Dlatego uważam, że te pojecia (integration test, unit test) nie przystają do rzeczywistości 2018. Jak się robiło wielkie kobyła klasy 10 lat temu, a testy z bazą IN MEMORY twały po 30 sekund to jak nabardziej taki podział miał sens. Teraz testy z bazą in mem to dwie linijki, a klasy dzieli się na tak małe, że testowanie w izolacji (jednostkowo) traci sens.

Przykład:
Jeśli testuję klasę typu DAO i ta klasa ma HashMapę (bo nasza baza danych to HashMapa), to czy wtedy to jest to test inegracyjny?
Żeby zrobić z tego unit test to muszę mockować hashmapę???

Jeśli teraz wezmę niewiele różniący się przypadek, gdzie nie trzymam danych w HashMapie tylko mam entityManager persist i find (zamiast put i get) ... to czy to nagle tak dużo zmienia ?

A co jak to stroluje i wydziele interfejs repozytorium z metodami put i get. I bede miał dwie implementacje HasMapa i Baza?

2

@danek

ktoś mi ostatnio powiedział, że skoro test zahacza o 2 klasy a nie jedną ze zmokowanymi zależnościami to już integracyjny

Nie ma ani jednej definicji, ani jednej granicy. "To zależy" jak se podzielisz testy w projekcie. Chociaż moim zdaniem nie ma sensu dzielić testów na podstawie tego czy zahaczają o 1 klasę czy o 10 klas naraz.

Już lepszy podziałby był np. na testy, które dotykają tylko kodu (i wszystko co nie dotyczy kodu, np. API Facebooka, mają zmockowane), i na testy, które dotykają np. bazy danych albo łączą się z realnymi API, np. z API Facebooka.

Albo podział na testy, które odpalają pojedyncze moduły gdzieś tam w kodzie (np. tworzą instancję klasy settings i patrzą czy settings.changeName() zmieni nazwę), i na testy, które testują GUI, patrzą czy po kliknięciu myszą w okienku Settings włączają się settingi itp.

Anyway, to i tak wszystko zależy. Chociaż z testami jednostkowymi jest niestety tak, że obrosły niezdrowym kultem i niektórym się wydaje, że to najszlachetniejszy rodzaj testu - mimo, że w tak zwanej dziczy* to testy które są najbardziej hiper-ultra jednostkowe są zwykle najbardziej nieprzydatne i nadają się głównie do kosza, bo ludzie testują w nich często szczegóły implementacyjne (które i tak się zmienią za miesiąc), a nie testują tego, czy moduł działa poprawnie w interakcji z innymi modułami.

* z angielska "in the wild" ;)

1

@LukeJL
Według mnie podstawowym rozdzieleniem powinno być na te testy które odpalasz z IDE (szybkie) co zmianę, żeby zobaczyć czy działa wszystko i te co odpalasz raz na jakieś czas (koniec taska, realise itp). Tak jak mówisz, testy jednostkowe to chyba już pewien kult.
Ba, powiedziałbym, że czasem nawet są szkodliwe. Mając napisane testy do każdej możliwej nieprywatnej klasy spróbuj coś bezboleśnie refaktoryzować ;)

0
danek napisał(a):

Ba, powiedziałbym, że czasem nawet są szkodliwe. Mając napisane testy do każdej możliwej nieprywatnej klasy spróbuj coś bezboleśnie refaktoryzować ;)

Test jednostkowy określa testowanie jednostki - nigdy nikt nie doprecyzował, że tu chodzi o klase, ale tak było łatwiej przyjąć. Testowanie modułu jako jednostki ma dla mnie dużo sensu i wtedy mockuję tylko zewnętrzne zależności modułu, a bazy jako in memory.

jarekr000000 napisał(a):

Przykład:
Jeśli testuję klasę typu DAO i ta klasa ma HashMapę (bo nasza baza danych to HashMapa), to czy wtedy to jest to test inegracyjny?
Żeby zrobić z tego unit test to muszę mockować hashmapę???

Jednostkowy, nie dotyka I/O, nie trzeba stawiać całego kontekstu aplikacji, jakiejś bazy embedded, która trzeba uruchomić co już zajmuje czas.

Jeśli teraz wezmę niewiele różniący się przypadek, gdzie nie trzymam danych w HashMapie tylko mam entityManager persist i find (zamiast put i get) ... to czy to nagle tak dużo zmienia ?

Oczywiście, że nie dla mnie wtedy test jednostkowy to zwykły test modułu + in-memory entityManager czy od bidy stub. Za to integracyjno-akceptacyjny gdzie chcesz sprawdzić czy twoja konfiguracja połączenia z bazą i pewnie jeszcze kontekstu aplikacji (troche spaczenie springiem) jest poprawna.

A co jak to stroluje i wydziele interfejs repozytorium z metodami put i get. I bede miał dwie implementacje HasMapa i Baza?

Wciąż jednostkowo zachowanie modułu i potem jeżeli chcesz to bazę i hashmapę ;) Też nie chcę twierdzić, że wszystko trzeba testować albo np. wszystkie scieżki czy na pewno baza zrobiła to co chciałeś, ale IMHO najbardziej krytyczne proste scenariusze warto mieć

0
hcubyc napisał(a):

Jednostkowy, nie dotyka I/O

Jeżeli mam klasę albo zestaw klas, które służą do operacji na plikach to nie mogę ich przetestować jednostkowo?

0

Możesz, ale tak jak napisałem dla mnie to będzie test integracyjny (tak jak napisał jarek - nie jest to precyzyjna nazwa, nie wymyśliłem lepszej, sorry, ale wtedy też testuję to razem z calym środowiskiem aplikacji). Test jednostkowy dla mnie jest ok, tylko dla operacji na plikach miałbym wystawiony interfejs i jednostkowo testował zachowanie, a nie to czy faktycznie np. się zapisuje plik. Nie jest to jakaś złota metoda tylko typowe aplikacje biznesowe, bo jeżeli masz klasę, której celem nie jest realizacja jakiejś logiki biznesowej tylko faktyczne operacje na plikach to wtedy to będzie twój test jednostkowy, ale ja takich klas piszę bardzo mało, dlatego traktuję je raczej jako wyjątek. W innym wypadku, gdy klasa/moduł/whatever realizuje jakąś logikę biznesową np. rejestrowanie faktur w systemie to fakt gdzie i jak to zapiszesz jest wymaganiem niefunkcjonalnym - wtedy IMHO I/O w teście jednostkowym jest słabe. Gdy piszesz coś czego celem jest I/O to wtedy nie mam z tym problemu

0

@Michał Sikora: to zależy czy testujesz wczytanie pliku czy logike obrabiania danych z pliku. W tym drugim przypadku nie potrzebujesz w ogóle pliku w testach

0

Dlatego zaznaczyłem, że chodzi mi o operacje na plikach. Np. piszę wrapper, który pozwala mi na szybki, atomowy zapis w kolejce do pliku. File jest tutaj dla mnie takim samym prymitywem jak np. int w teście assertEquals(4, 2 + 2) i jak najbardziej mogę pisać testy jednostkowe dla I/O w tym wypadku.

1

@danek:

ktoś mi ostatnio powiedział, że skoro test zahacza o 2 klasy a nie jedną ze zmokowanymi zależnościami to już integracyjny

To się mega łatwo da sprowadzic do absurdu. Jak twoja klasa korzysta z Integer, albo Stringa to już jest to test integracyjny?
Albo jak korzysta z jakiegoś prostego commons-lang, itp? Trudno tą granicę rozsądnie postawić.

Czasami test, który dużo ludzi uznałoby za jednostkowy i test, który dużo ludzi uznałoby za integracyjny wyglądają dokładnie tak samo:

https://github.com/javaFunAgain/ratpong/blob/master/src/test/java/pl/setblack/pongi/users/repo/UsersRepositoryTest.java

Trochę potworek, ale dokładnie chciałem pokazać ten case: mamy te same testy jak repo jest InMem (jakaś hashmapa, możemy powiedzieć, że Mock) i jak z faktyczną bazą.
(Choć baza nie jest SQLowa). Całość trwa milisekundy.

@hcubyc

Za to integracyjno-akceptacyjny gdzie chcesz sprawdzić czy twoja konfiguracja połączenia z bazą i pewnie jeszcze kontekstu aplikacji (troche spaczenie springiem) jest poprawna.

To, że w springu trzeba testować dodatkowo czy kontekst wstanie, bo ten sam kod działa zupełnie inaczej w kontekście springowym to jest dodatkowa patologia. To jest kolejny powód, dlaczego unikam springa w swoich aplikacjach. Co więcej już poznałem co najmniej 3 zespoły, które pod nazwą test integracyjny rozumiały test z konstektem springowym. Każdy zespół springowy szybko się uczy, że to że się kompiluje i testuje nic nie znaczy. Trzeba dodatkowo ten kontekst przećwiczyć (który wstaje mega wolno :-( ). Btw. w tym roku odkryłem, że nawet te testy z konstekstem springowym nie gwarantują, że aplikacja będzie się podobnie( jak w testach) na produkcji zachowywać.

Generalnie, z powodu nieprecyzyjności terminu uważam, że ma on średni sens, zbyt łatwo się wypacza. Takie nieprecyzyjne gnioty nie powinny być cześcią - computer science ani nawet software engineering (alem marzyciel). Wykłócanie się o to czy coś jest testem integracyjnym, jednostkowym, komponentowym, albo czy test jest jednostkowy czy nie, bo używa ConcurrentHashMap to mniej więcej tak jak wykłócanie się o pochodzenie ducha świętego: Filioque.

0

Piszesz testy dla publicznych metod, które z kolei korzystają z metod prywatnych. Wtedy metody prywatne zostaną tymi testami i tak pokryte. Nie ma tu żadnego problemu, nie rozumiem też dla czego chciałeś coś tutaj mockować.

Czy może jednak upublicznienie niektórych metod?

Testy nigdy nie powinny wymuszać takich rzeczy.

0
jarekr000000 napisał(a):

To, że w springu trzeba testować dodatkowo czy kontekst wstanie, bo ten sam kod działa zupełnie inaczej w kontekście springowym to jest dodatkowa patologia. To jest kolejny powód, dlaczego unikam springa w swoich aplikacjach. Co więcej już poznałem co najmniej 3 zespoły, które pod nazwą test integracyjny rozumiały test z konstektem springowym. Każdy zespół springowy szybko się uczy, że to że się kompiluje i testuje nic nie znaczy. Trzeba dodatkowo ten kontekst przećwiczyć (który wstaje mega wolno :-( )

Wiem o czym mówisz, ale miałem tu na mysli przetestowanie ścieżki krytycznej w działającym kontekście - a, że konfiguracja dobra, połączenie z bazą jest etc. Gdybym nie miał springa to też bym pisał takie testy.

Btw. w tym roku odkryłem, że nawet te testy z konstekstem springowym nie gwarantują, że aplikacja będzie się podobnie( jak w testach) na produkcji zachowywać.

Pochwal się odkryciem ;)

Generalnie, z powodu nieprecyzyjności terminu uważam, że ma on średni sens, zbyt łatwo się wypacza. Takie nieprecyzyjne gnioty nie powinny być cześcią - computer science ani nawet software engineering (alem marzyciel). Wykłócanie się o to czy coś jest testem integracyjnym, jednostkowym, komponentowym, albo czy test jest jednostkowy czy nie, bo używa ConcurrentHashMap to mniej więcej tak jak wykłócanie się o pochodzenie ducha świętego: Filioque.

Ciężko ci odmówić racji, ale co byś proponował w zamian? Nazwy na określenie testu, który testuje zachowanie nazwie się behawioralnym, a przypadek o którym pisałem wyżej akceptacyjny?

1
Aventus napisał(a):

Czy może jednak upublicznienie niektórych metod?

Testy nigdy nie powinny wymuszać takich rzeczy.

Jakby poszukać u Uncle Boba to kilkuktotnie powtarza on regułkę:

Tests trump Encapsulation

I doprecyzowuje, że jeśli to pomaga w testowaniu to trzeba rozluźniać dostęp.
Moje prywatne doświadczenie jest takie, że prawie wszystkie @VisibleForTesting to objawy kiepskiego designu klas. Zwykle da się przepisać.

0
hcubyc napisał(a):

Ciężko ci odmówić racji, ale co byś proponował w zamian? Nazwy na określenie testu, który testuje zachowanie nazwie się behawioralnym, a przypadek o którym pisałem wyżej akceptacyjny?

imo tak jak wspomniałem wcześniej, na szybkie które odpalasz z ide i trwają do ~3-5 sekund i te które odpalasz przy jakimś większym wydarzeniu (skończenie taska, nowa wersja itp)

3

W zasadzie cały ten wątek to zadawanie złego pytania na wstępie.

Zamiast pytać od razu szczegółowo o to "jak napisać testy do tej klasy, która ma dużo metod prywatnych" (pytanie o taktykę i skupianie się na konkretnej sytuacji, która jest niepowtarzalna i za tydzień będzie już inna klasa z innymi metodami i co, nowe pytanie na forum? ;) ), można zadawać pytania strategiczne, o przyczyny/sposób manifestowania się pewnych zjawisk:

  1. czemu niektóre testy jednostkowe w pewnych projektach trzeba co chwila zmieniać (no bo np. dlatego, że ludzie testują za dużo, np. sprawdzają liczbę rekurencji w liczbie, która oblicza silnię. W ten sposób zmiana algorytmu, np. usunięcie rekurencji, spowoduje, że test się rozwali). I tu się rodzi dalsze pytanie, o to czy warto testować szczegóły implementacji - ale tutaj już ludzie odpisali na to.
  2. jak otestować coś na co nie mamy wpływu, czy w ogóle warto to testować? (np. kontakt z API Facebooka, co z tym robić? Prawdziwe API? Jakieś mocki? Ręczne testowanie?)
  3. co testy w zasadzie testują? Czy przejście testów da nam jakiekolwiek zapewnienie, że aplikacja będzie działać poprawnie?
    https://natooktesting.files.wordpress.com/2017/08/unittest_faucet.gif
  4. jaki wpływ ma obecność testów na sposób pisania kodu? (w TDD miałyby znaczny, w przypadku OPa zerowy, ponieważ jak rozumiem to cały podsystem został już napisany, ileś klas napisanych, a teraz na siłe trzeba "dokleić" gdzieś testy. Moim zdaniem to trochę bez sensu. Lepszym podejściem było już pisząc kod produkcyjny, myśleć o tym, jak to potem otestować, bo wtedy nawet jeśli testy zostaną napisane po fakcie, to będziemy mogli łatwo to zrobić)

itp. można więcej takich pytań wymyślić, które byłyby bardziej sensowne niż pytania czysto techniczne typu:

Powermockito i mockowanie prywatnych metod? Niby nie powinno się tego nadużywać.
Testowanie samego API czyli metody getQuery()?
Czy może jednak upublicznienie niektórych metod?

Bo pytania techniczne nie dają całego kontekstu ani nie pokazują konsekwencji tego, czy innego podejścia. No i są niebezpieczne, bo uwaga człowieka zawęża się do szczegółów technicznych i można przeoczyć resztę.

1
hcubyc napisał(a):
jarekr000000 napisał(a):

Ciężko ci odmówić racji, ale co byś proponował w zamian? Nazwy na określenie testu, który testuje zachowanie nazwie się behawioralnym, a przypadek o którym pisałem wyżej akceptacyjny?

Uncle Bob ma lekko alternatywny koncept na nazywanie:
https://blog.cleancoder.com/uncle-bob/2017/05/05/TestDefinitions.html

Ale też mi się nie podoba.
Sam przestałem od jakiegoś czasu (nie tak dawno) dodawać te przymiotniki.
Skoro te wszystkie testy mają postać:

  • dla danych warunków początkowych,
  • wywołanie funkcji z danym inputem,
  • Powinnno dać oczekiwany rezultat.

Skoro używam tych samych narzędzi.
(Junit, KotlinTest, ScalaTest, Spoxk).
To po prostu nazywam to wszystko testem.

Ješli by trzeba było coś dodać, bo brzmi zbyt prostacko, to dodałbym jakiś przymiotnik, który nic nie wnosi lub nic nie znaczy:

  • testy automatyczne...,
  • testy weryfikacyjne :),
  • testy zwinne, agilne.

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