Unit Test - kiedy klasa jest zależna od innej?

Unit Test - kiedy klasa jest zależna od innej?
0

Cześć,

Mam takie pytanie związane z testami jednostkowymi. Testy jednostkowe zakładają żeby klasy testować wyizolowane od innych a w przypadku zależności do innych klas należy utworzyć prosta implementacje tych klas. Ale nie rozumiem kliku przykładów więc zwracam się do was o objaśnienia gdyż mój profesor od programowania nie był w stanie mi tego wyjaśnić.

Przykład pierwszy:

Kopiuj
public class SimpleClass {
    private OtherClass obj;

    public SimpleClass() {
        this.obj = new OtherClass();
    }

    public String doSomething() {
        obj.do();

         //reszta kodu
    }

    // gettery i settery
}

I pytanie, czy taka klasa jest zależna od innej? Od profesora się dowiedziałem, że nie jest gdyż jesteśmy w stanie ją przetestować bez tworzenia innych obiektów ("co było by testem integracyjnym"). Ale mi się wydaje, że test jednostkowy, który ma testować klasę, funkcję nie zależną od innych, będzie tutaj nie na miejscu bo przecież w klasie OtherObject w metodzie do() może być coś nie tak i w tedy test w SimpleKlas się nie powiedzie...

Już nie rozumiem tego...

Shalom
  • Rejestracja:około 21 lat
  • Ostatnio:prawie 3 lata
  • Lokalizacja:Space: the final frontier
  • Postów:26433
0

To zależy co to za OtherClass. Jeśli to jest inna "twoja" klasa to oczywiście test jednostkowy wymagałby mockowania tej klasy, bo jest ona zależnością. Ale jeśli ta klasa pochodzi z biblioteki albo to standardowa klasa javy to już niekoniecznie. Wtedy mockowałbyś tylko jeśli to by ci ułatwiło życie, albo żeby testować specjalne przypadki.


"Nie brookliński most, ale przemienić w jasny, nowy dzień najsmutniejszą noc - to jest dopiero coś!"
0

Tak zakładam, że to moja własna klasa... sory zapomniałem wcześniej o tym wspomnieć.

No ok w tym przykładzie, który podałem to jeszcze się da to zamokować, ale weźmy taki przykład.

Kopiuj
public class SimpleClass {
    private OtherClass  obj;

    public void doSomething() {
        obj = OtherClass();
        obj.do();

        //reszta kodu
    }
}

W takim przypadku nie jestem już w stanie przetestować tej metody w izolacji od innych...

Shalom
  • Rejestracja:około 21 lat
  • Ostatnio:prawie 3 lata
  • Lokalizacja:Space: the final frontier
  • Postów:26433
0
  1. To jest teraz kompozycja i pytanie brzmi czy w takim razie ma sens w ogóle testowanie tych dwóch klas osobno, skoro są tak mocno związane?
  2. A czemu dokładnie nie możesz? Porządne biblioteki do mockowania mają expectNew / whenNew i spokojnie mogą mockować także tworzenie obiektów.

"Nie brookliński most, ale przemienić w jasny, nowy dzień najsmutniejszą noc - to jest dopiero coś!"
edytowany 1x, ostatnio: Shalom
0

ad. 1 - Aha czyli takie klasy celowo napisane nie podlegają testom jednostkowym? Więc pisząc jakiś kod trzeba to robić tak aby dało się go później jak najlepiej przetestować?

ad. 2 - Problem widzę w tym: nawet jak zamockuję klase OtherObject i ustawię ją w SimpleObjects to po wywołaniu metody doSomething() i tak zostanie utworzony nowy realny obiekt.
Nie wiem do czego służą te metody, ja korzystam z frameworka Spock.

Shalom
  • Rejestracja:około 21 lat
  • Ostatnio:prawie 3 lata
  • Lokalizacja:Space: the final frontier
  • Postów:26433
0
  1. Między innymi warto mieć to na uwadze. Tak samo jak i to, że czasem implementacja może sie zmienić i fajnie byłoby móc ją podmienic bez zmian w 100 miejscach w kodzie bo masz wszędzie new. Po to mamy kontenery IoC, singletony, service locatory i generalnie wstrzykiwanie zależności. Jeśli dana klasa tworzy swoją zależność to znaczy że jest z nią ściśle związana.
  2. Na to już ci nic nie poradzę :P

"Nie brookliński most, ale przemienić w jasny, nowy dzień najsmutniejszą noc - to jest dopiero coś!"
KR
Moderator
  • Rejestracja:prawie 21 lat
  • Ostatnio:dzień
  • Postów:2964
3

W ogóle cała idea testowania jednej klasy jest głupia i nie wiem, po co tak kurczowo się tego trzymać. Kto powiedział, że w unit-testing unit == class?

QU
  • Rejestracja:prawie 11 lat
  • Ostatnio:około 10 lat
  • Postów:16
0

Generalnie są dwa nurty: klasyczny unit testing (restrykcyjny) w którym unit = klasa i nowy nurt, gdzie unit nie jest ściśle określony. Większość programistów używa tego drugiego, bo jest po prostu bardziej praktyczny. Nie widzę szczerze mówiąc zalet pierwszego podejścia do unit testów. Więcej możesz przeczytać o tym w artykule Martina Fowlera o testach jednostkowyhh na jego stronie.

somekind
Moderator
  • Rejestracja:około 17 lat
  • Ostatnio:około 5 godzin
  • Lokalizacja:Wrocław
0

To ja chyba jakiś dziwny jestem, bo dla mnie unit to zawsze była metoda.

Shalom
  • Rejestracja:około 21 lat
  • Ostatnio:prawie 3 lata
  • Lokalizacja:Space: the final frontier
  • Postów:26433
0

@somekind im raczej chodzi o to co mockować i uważać za "zewnętrzną" zależność. Ja zwykle też zakładam że unit=metoda, ale to wymaga czasem zaawansowanych narzędzi takich jak partial mock, no bo co zrobisz jak testowana metoda woła inną metodę tej klasy? Albo zostawisz to wywołanie, ale wtedy unit to juz nie jest jedna metoda, albo zrobisz częściowego mocka.


"Nie brookliński most, ale przemienić w jasny, nowy dzień najsmutniejszą noc - to jest dopiero coś!"
somekind
Moderator
  • Rejestracja:około 17 lat
  • Ostatnio:około 5 godzin
  • Lokalizacja:Wrocław
0

@Shalom, uściślając - dla mnie unit to jedna publiczna metoda.

Shalom
  • Rejestracja:około 21 lat
  • Ostatnio:prawie 3 lata
  • Lokalizacja:Space: the final frontier
  • Postów:26433
0

No dobra, ale jeśli ta publiczna metoda woła inną publiczną metodę to co? Partial mock czy jednak unit=klasa? ;)


"Nie brookliński most, ale przemienić w jasny, nowy dzień najsmutniejszą noc - to jest dopiero coś!"
somekind
Moderator
  • Rejestracja:około 17 lat
  • Ostatnio:około 5 godzin
  • Lokalizacja:Wrocław
0

Po pierwsze, to nie widzę potrzeby w wywoływaniu metod publicznych z innych publicznych w tej samej klasie. To jakaś lekka aberracja.
Po drugie - co za różnica, czy testowana metoda woła metody publiczne czy prywatne? Nadal testuję tę jedną metodę.

Shalom
  • Rejestracja:około 21 lat
  • Ostatnio:prawie 3 lata
  • Lokalizacja:Space: the final frontier
  • Postów:26433
1

@somekind no bez przesady. Prosty przykład: masz klasę Stack która ma metodę pop() i metodę empty(). Implementacja pop() sprawdza czy lista jest pusta i jeśli nie jest to zdejmuje element (a jeśli jest to rzuca wyjątek). I teraz jak chcesz testować tą metodę? Chcesz testować ją z wywołaniem prawdziwego empty()? Wtedy test może sie wyłożyć mimo że pop() jest poprawny, bo błąd jest w empty().
To czy wołasz metodę prywatną czy publiczną ma tylko takie znaczenie że większość ludzi nie testuje prywatnych metod (co uważam za głupie) i zakłada że testując publiczną metodę X testujesz jednocześnie jej wszystkie prywatne zależności.


"Nie brookliński most, ale przemienić w jasny, nowy dzień najsmutniejszą noc - to jest dopiero coś!"
edytowany 1x, ostatnio: Shalom
somekind
Moderator
  • Rejestracja:około 17 lat
  • Ostatnio:około 5 godzin
  • Lokalizacja:Wrocław
1
Shalom napisał(a):

@somekind no bez przesady. Prosty przykład: masz klasę Stack która ma metodę pop() i metodę empty(). Implementacja pop() sprawdza czy lista jest pusta i jeśli nie jest to zdejmuje element (a jeśli jest to rzuca wyjątek). I teraz jak chcesz testować tą metodę? Chcesz testować ją z wywołaniem prawdziwego empty()? Wtedy test może sie wyłożyć mimo że pop() jest poprawny, bo błąd jest w empty().

Wtedy test od empty też się wywali.

To czy wołasz metodę prywatną czy publiczną ma tylko takie znaczenie że większość ludzi nie testuje prywatnych metod (co uważam za głupie) i zakłada że testując publiczną metodę X testujesz jednocześnie jej wszystkie prywatne zależności.

To akurat oczywiste i jak najbardziej prawidłowe założenie. Jeśli metoda prywatna nie jest testowana podczas testów metod publicznych, to jest niepotrzebna, zaś jeśli jest tak skomplikowana, że potrzebuje własnych testów, to znaczy, że powinna być metodą publiczną w innej klasie.

Shalom
  • Rejestracja:około 21 lat
  • Ostatnio:prawie 3 lata
  • Lokalizacja:Space: the final frontier
  • Postów:26433
0

Założenie że tylko skomplikowane metody potrzebują testów jest bardzo złudne. Zresztą załóżmy że piszemy ten Stack ale uznaliśmy że empty() nie będzie w interfejsie tej klasy. Niemniej samo empty jednak mamy bo używamy go w kilku innych miejscach więc wydzielenie tej logiki do osobnej metody mialo sens (dodatkowo zwiększyło czytelność kodu). Ale sama logika tej metody jest prosta. I co teraz? Wydzielenie do osobnej klasy to jakiś overkill (chociaż faktycznie może mieć sens) a testowanie jej razem z metodami które jej używają niepotrzebnie komplikuje tamte testy i sprawia że są mniej czytelne.

Zresztą ja osobiście uważam że unit-testy zwykle niewiele testują a ich główną wartością jest to, że do słabego kodu testy pisze się po prostu trudno. Jak masz długie metody, albo z zagnieżdżonymi pętlami albo z N ifami to pisanie testów to masakra, bo samo konfigurowanie zachowania / konstruowanie danych wejściowych wymaga bardzo skomplikowanego i długiego kodu.


"Nie brookliński most, ale przemienić w jasny, nowy dzień najsmutniejszą noc - to jest dopiero coś!"
somekind
Moderator
  • Rejestracja:około 17 lat
  • Ostatnio:około 5 godzin
  • Lokalizacja:Wrocław
0
Shalom napisał(a):

Założenie że tylko skomplikowane metody potrzebują testów jest bardzo złudne.

A kto tak zakłada?
Ja napisałem, że jeśli metoda nie ma testów, to jest niepotrzebna, z czego wynika, że wszystkie metody potrzebują testów.

Wydzielenie do osobnej klasy to jakiś overkill (chociaż faktycznie może mieć sens) a testowanie jej razem z metodami które jej używają niepotrzebnie komplikuje tamte testy i sprawia że są mniej czytelne.

Skoro metoda jest prosta, to tak bardzo znowu tych testów nie skomplikuje. :)

Zresztą ja osobiście uważam że unit-testy zwykle niewiele testują

Mówisz teraz o swoich? :P

a ich główną wartością jest to, że do słabego kodu testy pisze się po prostu trudno. Jak masz długie metody, albo z zagnieżdżonymi pętlami albo z N ifami to pisanie testów to masakra, bo samo konfigurowanie zachowania / konstruowanie danych wejściowych wymaga bardzo skomplikowanego i długiego kodu.

Tak, to zaleta, ale bynajmniej nie główna.

Shalom
  • Rejestracja:około 21 lat
  • Ostatnio:prawie 3 lata
  • Lokalizacja:Space: the final frontier
  • Postów:26433
0

Mówisz teraz o swoich?

Bardzo możliwe że po prostu moje testy są słabe ;) Niemniej wielokrotnie zdarzało mi się poprawiać defekty mimo pełnego pokrycia kodu testami. Po prostu testy zwykle testują "czy kod działa dla danego przypadku testowego". Testy służą szukaniu błędów a nie dowodzeniu poprawności kodu. Jasne że w takiej sytuacji zawsze dopisujesz kolejny przypadek testowy, który zabezpiecza przed regresją dla znalezionego błędu, ale mimo to nadal nie ma pewności że jeszcze kiedyś się coś tam nie sypnie ;) Dlatego ja raczej z rezerwą podchodzę do unit-testów. Jak przechodzą to istnieje szansa że coś zadziała ;]


"Nie brookliński most, ale przemienić w jasny, nowy dzień najsmutniejszą noc - to jest dopiero coś!"
QU
  • Rejestracja:prawie 11 lat
  • Ostatnio:około 10 lat
  • Postów:16
1

Dla mnie jednostka to zachowanie dostępne publicznie. Jeżeli mam metodę do obliczania miejsc zerowych funkcji kwadratowej, to piszę testy tylko dla tej funkcji.
nie mockuje/stubuje operacji mnożenia, dodawania itd (wyolbrzymiając), bo:

  • jeżeli są to funkcje publiczne, to powinny być otestowane, ale nie w ramach testów mojej funkcji kwadratowej, tylko tam gdzie zostały zdefiniowane,
  • jeżeli są to funkcje napisane tylko na potrzeby mojej funkcji (np. obliczanie delty funkcji kwadratowej), czyli są prywatne dla jednostki (nie mylić z private w kodzie), to wtedy nie muszę jej testować, bo błędy i tak wyjdą w teście mojej funkcji. Pisał o tym też Evans (albo Vernon, już nie pamiętam) w DDD, że jednostką (unit of work) jest agregat a nie encja, chociaż, że encje to też instancje klas z publicznymi metodami. Jednak te encje i ich metody są prywatne dla agregatu, tj. tylko agregat może z nich korzystać. Dlatego otestowanie zachowań agregatu jest wystarczające.
edytowany 1x, ostatnio: quetzakubica
somekind
Moderator
  • Rejestracja:około 17 lat
  • Ostatnio:około 5 godzin
  • Lokalizacja:Wrocław
0
Shalom napisał(a):

Niemniej wielokrotnie zdarzało mi się poprawiać defekty mimo pełnego pokrycia kodu testami.

To zdanie stoi w sprzeczności samo ze sobą. Skoro są przypadki, których testy nie pokryły, to znaczy, że pokrycie nie było pełne.

Po prostu testy zwykle testują "czy kod działa dla danego przypadku testowego". Testy służą szukaniu błędów a nie dowodzeniu poprawności kodu.

Jeśli uwzględniają kilka przypadków typowych oraz wszystkie brzegowe, to dowodzą w wystarczającym stopniu. Znasz skuteczniejszą metodę dowiedzenia, że kod działa?

Dlatego ja raczej z rezerwą podchodzę do unit-testów. Jak przechodzą to istnieje szansa że coś zadziała ;]

To znaczy, że działają w testowanych przypadkach. Jeśli testowane są wszystkie przypadki, to soft będzie działać.

Cały czas piszę o sensownych testach, pisanych przed implementacją, a nie takich klepanych w ostatnim tygodniu trwania projektu, żeby dobić do 75% pokrycia testami, które PM obiecał klientowi.

Shalom
  • Rejestracja:około 21 lat
  • Ostatnio:prawie 3 lata
  • Lokalizacja:Space: the final frontier
  • Postów:26433
0

@somekind

  1. Pełne pokrycie oznacza tylko ze każda linia kodu jest wywołana przez jakiś test. Wcale nie oznacza to że wszystkie możliwe przypadki są sprawdzone. Nie ma takiej metryki bo i nie byłoby jak jej liczyć.
  2. Testy NIE dowodzą poprawności algorytmu. Są formalne metody dowodzenia poprawności, ale zwykle nie są zbyt praktyczne i się ich nie stosuje. Testy dowodzą że kod działa dla przypadków testowych. Jasne, że staramy się uwzględnić wszystkie klasy równoważności w teście, ale w praktyce nie zawsze da się to zrobić albo nie zawsze zrobimy to poprawnie. O testowaniu jakichś współbieżnych operacji to nawet nie wspominam ;]

Ja tez pisze o sensownych testach ;) Znam takich geniuszy co piszą jeden test który woła metodę na szczycie hierarchii (np. jakąś metodę kontrolera) i ona potem powoduje kaskadę wywołań przez pół projektu (bo np. ciągnie coś z bazy, woła serwisu do operacji na modelu etc) i potem się cieszą że napisali 5 linijek a miernik pokrycia pokazuje 60%. Nie mówie o takich sytuacjach ;)


"Nie brookliński most, ale przemienić w jasny, nowy dzień najsmutniejszą noc - to jest dopiero coś!"
KR
Takie testowanie jest ok, tylko inaczej się nazywa. ;-)
somekind
Moderator
  • Rejestracja:około 17 lat
  • Ostatnio:około 5 godzin
  • Lokalizacja:Wrocław
0
Shalom napisał(a):
  1. Pełne pokrycie oznacza tylko ze każda linia kodu jest wywołana przez jakiś test.

No, ale to nie jest pełne pokrycie kodu testami, tylko 100% linii pokrytych testami. Taka metryka dla PMów i innych humanistów.

Nie ma takiej metryki bo i nie byłoby jak jej liczyć.

Nie ma i nigdy nie będzie, bo nie da się żadnym automatem stwierdzić, czy kod jest sensownie napisany. To wszystko jest subiektywne i wymaga wiedzy eksperckiej.

  1. Testy NIE dowodzą poprawności algorytmu. Są formalne metody dowodzenia poprawności, ale zwykle nie są zbyt praktyczne i się ich nie stosuje.

Więc jedyną drogą to weryfikacji jest użycie testów.

Testy dowodzą że kod działa dla przypadków testowych. Jasne, że staramy się uwzględnić wszystkie klasy równoważności w teście, ale w praktyce nie zawsze da się to zrobić albo nie zawsze zrobimy to poprawnie.

I jeśli nie rozpatrzymy wszystkich przypadków, albo zrobimy to źle, to właśnie mamy niepełne testy, więc nie mamy też dowodu na poprawność naszego kodu. Wszystko się zgadza.

QU
  • Rejestracja:prawie 11 lat
  • Ostatnio:około 10 lat
  • Postów:16
0

Dobrym przykładem jest kod który symuluje, że Słońce krąży wokół Ziemi. Można to pokryć testami w 100% i wszystkie będą przechodzić a kod i tak będzie błędny, bo programista nie poznał dokładnie domeny problemu.

msm
Hmm, raczej to czy domena problemu jest dobrze zamodelowana można właśnie sprawdzić unit testami. Gorzej z przypadkami brzegowymi, których wszystkich nie da się przewidzieć.
KR
Moderator
  • Rejestracja:prawie 21 lat
  • Ostatnio:dzień
  • Postów:2964
1

Problem w tym, że bardzo często liczba wszystkich przypadków jest nieskończona i co wtedy? Nie da się przetestować wszystkich. Dlatego wolę testować zachowania i scenariusze a nie pojedyncze wywołania. Czasem unit to metoda, czasem klasa, a czasem kilka powiązanych klas, a czasem cały moduł składający się z kilkuset klas.

somekind
Możesz podać przykład takiej nieskończonej liczby przypadków?
KR
Wystarczy, że na wejściu jest kolekcja. A kolekcja nie musi być skończona.
somekind
Ale może nie istnieć, być pusta, albo zawierać skończoną liczbę elementów o istotnych z punktu widzenia algorytmu właściwościach. Chodziło mi o wszystkie sensowne przypadki, a nie wszystkie możliwe.
KR
Czyli masz nieskończoną liczbę możliwych danych wejściowych, z której programista musi wybrać skończoną, małą i sensowną liczbę przypadków. Pokrycie będzie zawsze wynosić 0 i Twoja metryka staje się bezużyteczna. Dlatego w praktyce mierzenie pokrycia jako "liczba wejść przetestowanych / liczba wszystkich możliwych wejść" jest niepraktyczna, i w praktyce stosuje się pokrycie linii kodu, o czym pisał poprzednik. Pokrycie linii kodu też ma swoje wady, ale jest na ogół dość blisko pokrycia wszystkich sensownych przypadków.
somekind
No bez jaj, można uzyskać 100% pokrycia kodu dwoma testami, które nie sprawdzą nawet 10% możliwych kombinacji parametrów. "Moja metryka" to liczba wejść przetestowanych / liczba wejść, które jest sens testować, a nie wszystkich.
somekind
Moderator
  • Rejestracja:około 17 lat
  • Ostatnio:około 5 godzin
  • Lokalizacja:Wrocław
0

Parę słów w temacie: http://www.pzielinski.com/?p=2432 i nawet się z tym zgadzam.

Shalom
  • Rejestracja:około 21 lat
  • Ostatnio:prawie 3 lata
  • Lokalizacja:Space: the final frontier
  • Postów:26433
1

@somekind no on tam nic wielce odkrywczego nie napisał ;) Tylko że moim zdaniem to się nie sprawdzi jeśli logika jest mocno rozbudowana, bo taki "scenariusz testowy" może wymagać współdziałania połowy modułów systemu i wysypanie się takiego testu niewiele nam powie. Poza tym to co on proponuje to typowy blackbox test, którym trudno jest objąć dużo przypadków. To bardziej taki sanity-test, który sprawdza czy jakieś znane standardowe scenariusze śmigają, ale nic więcej.
Z drugiej strony oczywiście nie należy przesadzać w drugą stronę i bawić się w pisanie testu, który sprawdza czy ciało danej metody jest takie jakie było kiedy pisano test. Widziałem takie "testy" -> sprawdzały kolejność, porządek i ilość wywołań wszystkiego w danej metodzie, w efekcie dowolna zmiana wymagała zmiany testu.


"Nie brookliński most, ale przemienić w jasny, nowy dzień najsmutniejszą noc - to jest dopiero coś!"
KR
Moderator
  • Rejestracja:prawie 21 lat
  • Ostatnio:dzień
  • Postów:2964
4

Są formalne metody dowodzenia poprawności, ale zwykle nie są zbyt praktyczne i się ich nie stosuje.

Każdy język statycznie typowany je stosuje, bo statyczny system typów nie jest niczym innym jak formalnym systemem dowodzenia poprawności kodu. Oczywiście systemy typów różnią się siłą i klasami poprawności, które są w stanie udowadniać. Np. Rust jest silniejszy niż C++, a Scala/Haskell silniejsze niż Java/C#. Natomiast każdy praktyczny formalny system posiada pewne ograniczenia, tzn. klasy błędów, których nie jest w stanie wykryć (lub udowodnić, że ich brak). I tam się zaczyne pole do popisu dla testowania.

Wracając do tematu - bardzo nie lubię dogmatycznego podejścia do testowania. Dogmaty z jakimi się spotkałem, które wyrządzają raczej więcej szkody niż pożytku:

  1. unit test musi zawsze testować jedną metodę / klasę / coś
  2. wszystkie zależności muszą być mockowane / stubowane
  3. łatwość pisania testów jest ważniejsza niż czytelność i prostota kodu (czyli wpychamy wszędzie DI tylko aby zadowolić pisaczy testów)
  4. testy koniecznie trzeba pisać zanim się napisze kod; jak się napisze odwrotnie, to robi się to źle
  5. kod o 100% pokryciu testami (wg linii) to dobrze przetestowany kod
  6. trzeba mieć minimum XX% pokrycia testami
  7. przechodzące testy dowodzą poprawności kodu (nie - jedynie dowodzą, że kod wykonuje prawidłowo przypadki testowe, i tylko te)
  8. statyczny system typów można zastąpić testami (szczególnie często wyznawane przez programistów języków dynamicznych)

Uważam, że jak do każdego innego tematu, do testowania trzeba podchodzić zdroworozsądkowo. Testowanie jest narzędziem a nie ideologią. Jeżeli testujesz pojedyncze metody i dzięki temu nabierasz pewności, że robią to co mają robić, i faktycznie wychwytujesz w nich ewentualne błędy to znaczy że robisz to dobrze. Jeżeli testujesz całe klasy albo moduły z wielu klas i dzięki temu łapiesz błędy przy refactoringu, to wszystko jest ok. Jeśli Twoje testy nie wyłapują błędów, ale pomagają młodym programistom zrozumieć, jak używa się twojego kodu - to też ma sens, i skoro to dla Ciebie działa, to czemu miałbyś tego tak nie robić? Jeżeli napisałeś 10000 testów w jakiś określony sposób, bo jakiś guru w Internetach napisał, że tylko tak jest poprawnie; ale te testy nie złapały ani jednego błędu, ani właściwie nikomu nie są potrzebne (no może tylko szefowi, żeby miał poczucie jaki to nowoczesny zespół ma działający wg najnowszych metodyk) to coś jest nie tak i lepiej się zastanowić.

edytowany 6x, ostatnio: Krolik
Wibowit
  • Rejestracja:prawie 20 lat
  • Ostatnio:około 7 godzin
0
Krwawy Młot napisał(a):

Cześć,

Mam takie pytanie związane z testami jednostkowymi. Testy jednostkowe zakładają żeby klasy testować wyizolowane od innych a w przypadku zależności do innych klas należy utworzyć prosta implementacje tych klas. Ale nie rozumiem kliku przykładów więc zwracam się do was o objaśnienia gdyż mój profesor od programowania nie był w stanie mi tego wyjaśnić.

Przykład pierwszy:

Kopiuj
public class SimpleClass {
    private OtherClass obj;

    public SimpleClass() {
        this.obj = new OtherClass();
    }

    public String doSomething() {
        obj.do();

         //reszta kodu
    }

    // gettery i settery
}

I pytanie, czy taka klasa jest zależna od innej? Od profesora się dowiedziałem, że nie jest gdyż jesteśmy w stanie ją przetestować bez tworzenia innych obiektów ("co było by testem integracyjnym"). Ale mi się wydaje, że test jednostkowy, który ma testować klasę, funkcję nie zależną od innych, będzie tutaj nie na miejscu bo przecież w klasie OtherObject w metodzie do() może być coś nie tak i w tedy test w SimpleKlas się nie powiedzie...

Już nie rozumiem tego...

Tutaj jest zależność na stałe od OtherClass, która jest konkretną produkcyjną implementacją. Żeby to obejść można np stworzyć dodatkowy konstruktor, który przyjmuje OtherClass jako zależność i wtedy można ją mockować/ podstawiać (np testową implementacją która jest współdzielona przez wiele testów)/ cokolwiek.

Sorry jeśli już to zostało napisane :)


"Programs must be written for people to read, and only incidentally for machines to execute." - Abelson & Sussman, SICP, preface to the first edition
"Ci, co najbardziej pragną planować życie społeczne, gdyby im na to pozwolić, staliby się w najwyższym stopniu niebezpieczni i nietolerancyjni wobec planów życiowych innych ludzi. Często, tchnącego dobrocią i oddanego jakiejś sprawie idealistę, dzieli od fanatyka tylko mały krok."
Demokracja jest fajna, dopóki wygrywa twoja ulubiona partia.
somekind
Moderator
  • Rejestracja:około 17 lat
  • Ostatnio:około 5 godzin
  • Lokalizacja:Wrocław
1

@Krolik, z większością się zgadzam, ale:

  1. testy koniecznie trzeba pisać zanim się napisze kod; jak się napisze odwrotnie, to robi się to źle

To jest prawda w pewnym sensie.

  1. Pisanie testów przed implementacją to zysk na czasie debugowania właściwej implementacji. Pisząc testy "po" nie ma tego zysku, więc mają one mniejszy sens.
  2. Pisząc testy przed łatwiej je napisać skrupulatnie, zastanawiając się, co metoda ma przyjąć, i co zwrócić. Takie testy testują sensowne przypadki testowe, błędne dane i warunki brzegowe. Pisanie testów "po" zazwyczaj kończy się przetestowaniem jedynie kilku optymistycznych ścieżek. Takie testy są nic nie warte.
  3. Pisanie czegoś, w co się wierzy, że jest przydatne sprawia, że programista przynajmniej stara się zrobić to dobrze.. Jeśli pisze się testy "po", często kończy się to klasami testowymi będącymi ogromnymi spaghetti godobjectami, bo są one tylko "dodatkiem" do aplikacji, a nie metodą jej wytwarzania.
  1. kod o 100% pokryciu testami (wg linii) to dobrze przetestowany kod
  2. trzeba mieć minimum XX% pokrycia testami

Tu jestem skonfundowany. W poście piszesz takie rzeczy, a w komentarzach się ze mną kłócisz, gdy piszę, że ta miara jest bez sensu.

  1. przechodzące testy dowodzą poprawności kodu (nie - jedynie dowodzą, że kod wykonuje prawidłowo przypadki testowe, i tylko te)

Nie dowodzą w sensie matematycznym, bo i nie ma sensu takiego dowodzenia w praktyczniej dziedzinie. Tworzenie oprogramowania to jakby nie patrzeć inżynieria, teoria jest tylko jednym z jej filarów, równie ważne są doświadczenie, intuicja i praktyczny zmysł nie robienia rzeczy bezsensownych tylko dlatego, że teoretycy mówią, że tak należy.

Rozumiem, że ktoś się nie zgadza ze mną, gdy twierdzę, że kod działa, czego dowodem są testy zwracające oczekiwane rezultaty dla sensownych zestawów danych wejściowych. Nie ma nic łatwiejszego niż obalić to twierdzenie podając dane, dla których kod nie zadziała.

Jeżeli napisałeś 10000 testów w jakiś określony sposób, bo jakiś guru w Internetach napisał, że tylko tak jest poprawnie; ale te testy nie złapały ani jednego błędu

No i to jest właśnie powód do pisania testów przed implementacją.

KR
Moderator
  • Rejestracja:prawie 21 lat
  • Ostatnio:dzień
  • Postów:2964
2

Co do pisania testów przed implementacją, to ja mogę podać kilka argumentów przeciw:

  1. Tylko w idealnym świecie oraz książkach interfejs jest całkowicie niezależny od implementacji. W praktyce jednak implementacja ma wpływ na interfejs, a interfejs na implementację. Myśląc tylko o samym interfejsie, możemy zaprojektować taki interfejs, że implementacja będzie 3x bardziej skomplikowana niż mogłaby być. Możemy też spowodować problemy z wydajnością - np. konieczność przekształcania formatu danych do takiego jaki zażyczył sobie projektant interfejsu, albo problemy na styku sync/async. W efekcie pisząc testy pod interfejs zaprojektowany w oderwaniu od implementacji może się okazać, że w trakcie pisania implementacji będziemy musieli zmienić interfejs, a w efekcie przepisać testy. Innymi słowy - marnujemy czas na przepisywanie testów kilka razy.

  2. Pisanie implementacji pod gotowy zestaw testów powoduje u niektórych programistów chęć "zaliczenia" wszytkich testów byle jak, byle tylko na końcu było zielone. W efekcie czytelność i prostota implementacji pozostawia wiele do życzenia. Np. widziałem kod ogólnie całkiego dobrego programisty, który folgując TDD, napisał implementację odzwierciedlającą po prostu po kolei każdy przypadek testowy jakimś wielkim switchem czy drabinką ifów, zamiast uogólnić wszystkie przypadki. A ponieważ w testach brakowało dwóch szczególnych przypadków, a jeden był na dodatek źle, to i kod miał analogiczne błędy.

  3. Testy przed implementacją wymuszają programowanie/projektowanie stylem top-down. W efekcie można spędzić 5 dni na pisanie kodu i dalej nie mieć nic działającego, prócz kilku pięknych warstw abstrakcji. Ja tu widzę pewien paradoks, bo top-down jest bardzo, bardzo "nie-agile". Tymczasem wielu programistów, których podziwiam, i którzy piszą bardzo dobrze zaprojektowany kod, preferują bottom-up (np. Paul Graham, Peter Norvig, Ken Thompson, Linus Torvalds).
    http://www.paulgraham.com/progbot.html

Pisząc testy przed łatwiej je napisać skrupulatnie, zastanawiając się, co metoda ma przyjąć, i co zwrócić. Takie testy testują sensowne przypadki testowe, błędne dane i warunki brzegowe

Ten argument raczej działa na korzyść pisania testów po, a nie przed. Nie znając implementacji, nie jesteś w stanie określić prawidłowo wszystkich przypadków brzegowych, bo mogą być przypadki brzegowe ukryte, wynikające z implementacji. Przypadkiem brzegowym mogą być np. dane wejściowe o wielkości dokładnie 65536 B, bo implementacja używa bufora o takiej dokładnie wielkości i pechowo tylko dla tej wartości jest off-by-one error... Pisząc testy przed implementacją nie masz szans takich rzeczy sprawdzić.

Pisanie testów "po" zazwyczaj kończy się przetestowaniem jedynie kilku optymistycznych ścieżek. Takie testy są nic nie warte.

Test testujący kilka optymistycznych ścieżek może nie jest kompletny, ale też ma pewną wartość. Zwłaszcza jeśli te optymistyczne ścieżki to akurat te, które są wykorzystywane przez klientów. Taki test nadal chroni przed regresjami.

  1. trzeba mieć minimum XX% pokrycia testami
    Tu jestem skonfundowany. W poście piszesz takie rzeczy, a w komentarzach się ze mną kłócisz, gdy piszę, że ta miara jest bez sensu.

Sama miara nie jest bez sensu. Natomiast bez sensu jest dogmatyczne używanie jej jako jedynego wyznacznika jakości testów. Pokrycie linijkowe jest użyteczne w celu pokazania, które fragmenty kodu wymagają jeszcze testów. Ale nie jest użyteczne określenie, że ma być pokrycie 80% i ani pół procenta mniej.

Nie dowodzą w sensie matematycznym, bo i nie ma sensu takiego dowodzenia w praktyczniej dziedzinie.

Statyczny system typów dowodzi braku błędów (pewnej klasy) dla wszystkich możliwych wejść, nawet jeśli liczba ta jest nieskończona, w sensie matematycznym. Dysponując silnym systemem typów mogę pisać tak kod, aby zminimalizować zapotrzebowanie na testy. Stąd testy powinno się robić po, aby pokryć tylko te przypdadki, których nie dało się uwzględnić w systemie typów / analizą statyczną. Na przykład, jeśli system typów potrafi udowodnić, że dana metoda nigdy nie zwraca null, to nie ma sensu pisać testów, które to testują - będzie to marnowanie czasu. Oczywiście w praktyce system typów nie potrafi udowodnić wszystkiego, więc testy są nadal użyteczne.

Jeśli pisze się testy "po", często kończy się to klasami testowymi będącymi ogromnymi spaghetti godobjectami, bo są one tylko "dodatkiem" do aplikacji, a nie metodą jej wytwarzania.

Testy dodatkiem do aplikacji, a nie są metodą jej wytwarzania. Testy to też jest kod, który trzeba wytworzyć i utrzymać. Im mniej, aby osiągnąć cel, tym lepiej. Testy są użytecznym narzędziem, ale są tylko jednym z wielu narzędzi zapewnienia odpowiedniej jakości projektu. Robienie z nich centralnego elementu wytwarzania oprogramowania to jakieś nieporozumienie. Klienta nie obchodzą testy, klienta obchodzi działające oprogramowanie.

edytowany 3x, ostatnio: Krolik
Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)